Skip to content

Releases: bigcommerce/catalyst

@bigcommerce/catalyst-makeswift@1.4.2

27 Jan 21:14
5a33677

Choose a tag to compare

Patch Changes

@bigcommerce/catalyst-makeswift@1.4.1

27 Jan 17:39
19b7991

Choose a tag to compare

Patch Changes

@bigcommerce/catalyst-core@1.4.2

27 Jan 19:06
b435d3c

Choose a tag to compare

@bigcommerce/catalyst-core@1.4.1

26 Jan 22:19
81b13e4

Choose a tag to compare

Patch Changes

  • #2827 49b1097 Thanks @jorgemoya! - Filter out child cart items (items with parentEntityId) from cart and cart analytics to prevent duplicate line items when products have parent-child relationships, such as product bundles.

    Migration steps

    Step 1: GraphQL Fragment Updates

    The parentEntityId field has been added to both physical and digital cart item fragments to identify child items.

    Update core/app/[locale]/(default)/cart/page-data.ts:

      export const PhysicalItemFragment = graphql(`
        fragment PhysicalItemFragment on CartPhysicalItem {
          entityId
          quantity
          productEntityId
          variantEntityId
    +     parentEntityId
          listPrice {
            currencyCode
            value
          }
        }
      `);
    
      export const DigitalItemFragment = graphql(`
        fragment DigitalItemFragment on CartDigitalItem {
          entityId
          quantity
          productEntityId
          variantEntityId
    +     parentEntityId
          listPrice {
            currencyCode
            value
          }
        }
      `);

    Step 2: Cart Display Filtering

    Cart line items are now filtered to exclude child items when displaying the cart.

    Update core/app/[locale]/(default)/cart/page.tsx:

      const lineItems = [
        ...cart.lineItems.giftCertificates,
        ...cart.lineItems.physicalItems,
        ...cart.lineItems.digitalItems,
    - ];
    + ].filter((item) => !('parentEntityId' in item) || !item.parentEntityId);

    Step 3: Analytics Data Filtering

    Analytics data collection now only includes top-level items to prevent duplicate tracking.

    Update core/app/[locale]/(default)/cart/page.tsx in the getAnalyticsData function:

    - const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems];
    + const lineItems = [...cart.lineItems.physicalItems, ...cart.lineItems.digitalItems].filter(
    +   (item) => !item.parentEntityId, // Only include top-level items
    + );

    Step 4: Styling Update

    Cart subtitle text color has been updated for improved contrast.

    Update core/vibes/soul/sections/cart/client.tsx:

    -                  <span className="text-[var(--cart-subtext-text,hsl(var(--contrast-300)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]">
    +                  <span className="text-[var(--cart-subtext-text,hsl(var(--contrast-400)))] contrast-more:text-[var(--cart-subtitle-text,hsl(var(--contrast-500)))]">
                         {lineItem.subtitle}
                       </span>
  • #2811 b57bffa Thanks @chanceaclark! - Fix pagination cursor persistence when changing sort order. The before and after query parameters are now cleared when the sort option changes, preventing stale pagination cursors from causing incorrect results or empty pages.

  • #2833 a520dbc Thanks @jamesqquick! - Add placeholders for gift certificate inputs and remove redundant placeholders in the gift certificate purchase form.

  • #2818 74e4dd1 Thanks @jordanarldt! - Disable product filters that are no longer available based on the selection.

    Migration steps

    Step 1

    Update the facetsTransformer function in core/data-transformers/facets-transformer.ts to handle disabled filters:

      return allFacets.map((facet) => {
        const refinedFacet = refinedFacets.find((f) => f.displayName === facet.displayName);
    
    +    if (refinedFacet == null) {
    +      return null;
    +    }
    +
        if (facet.__typename === 'CategorySearchFilter') {
          const refinedCategorySearchFilter =
    -        refinedFacet?.__typename === 'CategorySearchFilter' ? refinedFacet : null;
    +        refinedFacet.__typename === 'CategorySearchFilter' ? refinedFacet : null;
    
          return {
            type: 'toggle-group' as const,
            paramName: 'categoryIn',
            label: facet.displayName,
            defaultCollapsed: facet.isCollapsedByDefault,
            options: facet.categories.map((category) => {
              const refinedCategory = refinedCategorySearchFilter?.categories.find(
                (c) => c.entityId === category.entityId,
              );
              const isSelected = filters.categoryEntityIds?.includes(category.entityId) === true;
    +          const disabled = refinedCategory == null && !isSelected;
    +          const productCountLabel = disabled ? '' : ` (${category.productCount})`;
    +          const label = facet.displayProductCount
    +            ? `${category.name}${productCountLabel}`
    +            : category.name;
    
              return {
    -            label: facet.displayProductCount
    -              ? `${category.name} (${category.productCount})`
    -              : category.name,
    +            label,
                value: category.entityId.toString(),
    -            disabled: refinedCategory == null && !isSelected,
    +            disabled,
              };
            }),
          };
        }
    
        if (facet.__typename === 'BrandSearchFilter') {
          const refinedBrandSearchFilter =
    -        refinedFacet?.__typename === 'BrandSearchFilter' ? refinedFacet : null;
    +        refinedFacet.__typename === 'BrandSearchFilter' ? refinedFacet : null;
    
          return {
            type: 'toggle-group' as const,
            paramName: 'brand',
            label: facet.displayName,
            defaultCollapsed: facet.isCollapsedByDefault,
            options: facet.brands.map((brand) => {
              const refinedBrand = refinedBrandSearchFilter?.brands.find(
                (b) => b.entityId === brand.entityId,
              );
              const isSelected = filters.brandEntityIds?.includes(brand.entityId) === true;
    +          const disabled = refinedBrand == null && !isSelected;
    +          const productCountLabel = disabled ? '' : ` (${brand.productCount})`;
    +          const label = facet.displayProductCount
    +            ? `${brand.name}${productCountLabel}`
    +            : brand.name;
    
              return {
    -            label: facet.displayProductCount ? `${brand.name} (${brand.productCount})` : brand.name,
    +            label,
                value: brand.entityId.toString(),
    -            disabled: refinedBrand == null && !isSelected,
    +            disabled,
              };
            }),
          };
        }
    
        if (facet.__typename === 'ProductAttributeSearchFilter') {
          const refinedProductAttributeSearchFilter =
    -        refinedFacet?.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null;
    +        refinedFacet.__typename === 'ProductAttributeSearchFilter' ? refinedFacet : null;
    
          return {
            type: 'toggle-group' as const,
            paramName: `attr_${facet.filterKey}`,
            label: facet.displayName,
            defaultCollapsed: facet.isCollapsedByDefault,
            options: facet.attributes.map((attribute) => {
              const refinedAttribute = refinedProductAttributeSearchFilter?.attributes.find(
                (a) => a.value === attribute.value,
              );
    
              const isSelected =
                filters.productAttributes?.some((attr) => attr.values.includes(attribute.value)) ===
                true;
    
    +          const disabled = refinedAttribute == null && !isSelected;
    +          const productCountLabel = disabled ? '' : ` (${attribute.productCount})`;
    +          const label = facet.displayProductCount
    +            ? `${attribute.value}${productCountLabel}`
    +            : attribute.value;
    +
              return {
    -            label: facet.displayProductCount
    -              ? `${attribute.value} (${attribute.productCount})`
    -              : attribute.value,
    +            label,
                value: attribute.value,
    -            disabled: refinedAttribute == null && !isSelected,
    +            disabled,
              };
            }),
          };
        }
    
        if (facet.__typename === 'RatingSearchFilter') {
          const refinedRatingSearchFilter =
    -        refinedFacet?.__typename === 'RatingSearchFilter' ? refinedFacet : null;
    +        refinedFacet.__typename === 'RatingSearchFilter' ? refinedFacet : null;
          const isSelected = filters.rating?.minRating != null;
    
          return {
            type: 'rating' as const,
            paramName: 'minRating',
            label: facet.displayName,
            disabled: refinedRatingSearchFilter == null && !isSelected,
            defaultCollapsed: facet.isCollapsedByDefault,
          };
        }
    
        if (facet.__typename === 'PriceSearchFilter') {
          const refinedPriceSearchFilter =
    -        refinedFacet?.__typename === 'PriceSearchFilter' ? refinedFacet : null;
    +        refinedFacet.__typename === 'PriceSearchFilter' ? refinedFacet : null;
          const isSelected = filters.price?.minPrice != null || filters.price?.maxPrice != null;
    
          return {
            type: 'range' as const,
            minParamName: 'minPrice',
            maxPara...
Read more

@bigcommerce/catalyst-makeswift@1.4.0

05 Jan 23:21
46cf4bd

Choose a tag to compare

Minor Changes

Patch Changes

  • #2791 bd30ed3 Thanks @migueloller! - Fix sort order of additionalProducts prop in ProductsCarousel Makeswift component.

@bigcommerce/catalyst-core@1.4.0

05 Jan 22:39
44c682e

Choose a tag to compare

Minor Changes

  • #2806 becb67d Thanks @chanceaclark! - Upgrade c15t to 1.8.2, migrate from custom mode to offline mode, refactor consent cookie handling to use c15t's compact format, add script location support for HEAD/BODY rendering, and add privacy policy link support to CookieBanner.

    What Changed

    • Upgraded @c15t/nextjs to version 1.8.2
    • Changed consent manager mode from custom (with endpoint handlers) to offline mode
      • Removed custom handlers.ts implementation
    • Added enabled prop to C15TConsentManagerProvider to control consent manager functionality
    • Removed custom consent cookie encoder/decoder implementations (decoder.ts, encoder.ts)
    • Added parse-compact-format.ts to handle c15t's compact cookie format
      • Compact format: i.t:timestamp,c.necessary:1,c.functionality:1,etc...
    • Updated cookie parsing logic in both client and server to use the new compact format parser
    • Scripts now support location field from BigCommerce API and can be rendered in <head> or <body> based on the target property
    • CookieBanner now supports the privacyPolicyUrl field from BigCommerce API and will be rendered in the banner description if available.

    Migration Path

    Consent Manager Provider Changes

    The ConsentManagerProvider now uses offline mode instead of custom mode with endpoint handlers. The provider configuration has been simplified:

    Before:

    <C15TConsentManagerProvider
      options={{
        mode: 'custom',
        consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
        endpointHandlers: {
          showConsentBanner: () => showConsentBanner(isCookieConsentEnabled),
          setConsent,
          verifyConsent,
        },
      }}
    >
      <ClientSideOptionsProvider scripts={scripts}>
        {children}
      </ClientSideOptionsProvider>
    </C15TConsentManagerProvider>

    After:

    <C15TConsentManagerProvider
      options={{
        mode: 'offline',
        storageConfig: {
          storageKey: CONSENT_COOKIE_NAME,
          crossSubdomain: true,
        },
        consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
        enabled: isCookieConsentEnabled,
      }}
    >
      <ClientSideOptionsProvider scripts={scripts}>
        {children}
      </ClientSideOptionsProvider>
    </C15TConsentManagerProvider>

    Key changes:

    • mode changed from 'custom' to 'offline'
    • Removed endpointHandlers - no longer needed in offline mode
    • Added enabled prop to control consent manager functionality
    • Added storageConfig for cookie storage configuration

    Cookie Handling

    If you have custom code that directly reads or writes consent cookies, you'll need to update it:

    Before:
    The previous implementation used custom encoding/decoding. If you were directly accessing consent cookie values, you would have needed to use the custom decoder.

    After:
    The consent cookie now uses c15t's compact format. The public API for reading cookies remains the same:

    import { getConsentCookie } from '~/lib/consent-manager/cookies/client'; // client-side
    // or
    import { getConsentCookie } from '~/lib/consent-manager/cookies/server'; // server-side
    
    const consent = getConsentCookie();

    The getConsentCookie() function now internally uses parseCompactFormat() to parse the compact format cookie string. If you were directly parsing cookie values, you should now use the getConsentCookie() helper instead.

    getConsentCookie now returns a compact version of the consent values:

    {
      i.t: 123456789,
      c.necessary: true,
      c.functionality: true,
      c.marketing: false,
      c.measurment: false
    }

    Updated instances where getConsentCookie is used to reflect this new schema.

    Removed setConsentCookie from server and client since this is now handled by the c15t library.

    Script Location Support

    Scripts now support rendering in either <head> or <body> based on the location field from the BigCommerce API:

    // Scripts transformer now includes target based on location
    target: script.location === 'HEAD' ? 'head' : 'body';

    The ScriptsFragment GraphQL query now includes the location field, allowing scripts to be placed in the appropriate DOM location. FOOTER location is still not supported.

    Privacy Policy

    The RootLayoutMetadataQuery GraphQL query now includes the privacyPolicyUrl field, which renders a provicy policy link in the CookieBanner description.

    <CookieBanner
      privacyPolicyUrl="https://example.com/privacy-policy"
      // ... other props
    />

    The privacy policy link:

    • Opens in a new tab (target="_blank")
    • Only renders if privacyPolicyUrl is provided as a non-empty string

    Add translatable privacyPolicy field to Components.ConsentManager.CookieBanner translation namespace for the privacy policy link text.

  • #2806 becb67d Thanks @chanceaclark! - Conditionally display product ratings in the storefront based on site.settings.display.showProductRating. The storefront logic when this setting is enabled/disabled matches exactly the logic of Stencil + Cornerstone.

  • #2806 becb67d Thanks @chanceaclark! - Adds product review submission functionality to the product detail page via a modal form with validation for rating, title, review text, name, and email fields. Integrates with BigCommerce's GraphQL API using Conform and Zod for form validation and real-time feedback.

  • #2806 becb67d Thanks @chanceaclark! - Introduce displayName and displayKey fields to facets for improved labeling and filtering

    Facet filters now use the displayName field for more descriptive labels in the UI, replacing the deprecated name field. Product attribute facets now support the filterKey field for consistent parameter naming. The facet transformer has been updated to use displayName with a fallback to filterName when displayName is not available.

  • #2806 becb67d Thanks @chanceaclark! - Updated product and brand pages to include the number of reviews in the product data. Fixed visual spacing within product cards. Enhanced the Rating component to display the number of reviews alongside the rating. Introduced a new RatingLink component for smooth scrolling to reviews section on PDP.

  • #2806 becb67d Thanks @chanceaclark! - Make newsletter signup component on homepage render conditionally based on BigCommerce settings.

    What Changed

    • Newsletter signup component (Subscribe) on homepage now conditionally renders based on showNewsletterSignup setting from BigCommerce.
    • Added showNewsletterSignup field to HomePageQuery GraphQL query to fetch newsletter settings.
    • Newsletter signup now uses Stream component with Streamable pattern for progressive loading.

    Migration

    To make newsletter signup component render conditionally based on BigCommerce settings, update your homepage code:

    1. Update GraphQL Query (page-data.ts)

    Add the newsletter field to your HomePageQuery:

    const HomePageQuery = graphql(
      `
        query HomePageQuery($currencyCode: currencyCode) {
          site {
            // ... existing fields
            settings {
              inventory {
                defaultOutOfStockMessage
                showOutOfStockMessage
                showBackorderMessage
              }
              newsletter {
                showNewsletterSignup
              }
            }
          }
        }
      `,
      [FeaturedProductsCarouselFragment, FeaturedProductsListFragment],
    );

    2. Update Homepage Component (page.tsx)

    Import Stream and create a streamable for newsletter settings:

    import { Stream, Streamable } from '@/vibes/soul/lib/streamable';
    
    // Inside your component, create the streamable:
    const streamableShowNewsletterSignup = Streamable.from(async () => {
      const data = await streamablePageData;
      const { showNewsletterSignup } = data.site.settings?.newsletter ?? {};
      return showNewsletterSignup;
    });
    
    // Replace direct rendering with conditional Stream:
    <Stream fallback={null} value={streamableShowNewsletterSignup}>
      {(showNewsletterSignup) => showNewsletterSignup && <Subscribe />}
    </Stream>

    Before:

    <Subscribe />

    After:

    <Stream fallback={null} value={streamableShowNewsletterSignup}>
      {(showNewsletterSignu...
Read more

@bigcommerce/catalyst-makeswift@1.3.8

12 Dec 20:17
7fd9b69

Choose a tag to compare

Patch Changes

  • #2773 b475a36 Thanks @chanceaclark! - Catalyst has been upgraded to Next.js 15.5.9. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.

    🔒 Security Update

    This upgrade addresses a security vulnerability (CVE-2025-55184 + CVE-2025-55183) that affects React Server Components. These vulnerabilities allow a Denial of Service attack and Source Code Exposure attach. This upgrade includes:

    • Next.js 15.5.9 with the security patch
    • React 19.1.4 and React DOM 19.1.4 with the security patch

    All users are strongly encouraged to upgrade immediately.

    Key Changes

    • Next.js 15.5.9: Upgraded from Next.js 15.5.7 to 15.5.9
    • ⚛️ React 19: Upgraded to React 19.1.4 and React DOM 19.1.4

    Migration Guide

    Update Dependencies

    If you're maintaining a custom Catalyst store, update your package.json:

    {
      "dependencies": {
        "next": "15.5.9",
        "react": "19.1.4",
        "react-dom": "19.1.4"
      },
      "devDependencies": {
        "@next/bundle-analyzer": "15.5.9",
        "eslint-config-next": "15.5.9"
      }
    }

    Then run:

    pnpm install

@bigcommerce/catalyst-makeswift@1.3.7

12 Dec 00:06
3dc74a1

Choose a tag to compare

Patch Changes

  • #2764 83c5b75 Thanks @chanceaclark! - # Next.js 15.5.8 Upgrade

    Catalyst has been upgraded to Next.js 15.5.8. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.

    🔒 Critical Security Update

    This upgrade addresses a critical security vulnerability (CVE-2025-55184 + CVE-2025-55183) that affects React Server Components. These vulnerabilities allow a Denial of Service attack and Source Code Exposure attach. This upgrade includes:

    • Next.js 15.5.8 with the security patch
    • React 19.1.3 and React DOM 19.1.3 with the security patch

    All users are strongly encouraged to upgrade immediately.

    Key Changes

    • Next.js 15.5.8: Upgraded from Next.js 15.5.7 to 15.5.8
    • ⚛️ React 19: Upgraded to React 19.1.3 and React DOM 19.1.3

    Migration Guide

    Update Dependencies

    If you're maintaining a custom Catalyst store, update your package.json:

    {
      "dependencies": {
        "next": "15.5.8",
        "react": "19.1.3",
        "react-dom": "19.1.3"
      },
      "devDependencies": {
        "@next/bundle-analyzer": "15.5.8",
        "eslint-config-next": "15.5.8"
      }
    }

    Then run:

    pnpm install

@bigcommerce/catalyst-core@0.24.4

12 Dec 20:31
eab0b9b

Choose a tag to compare

Patch Changes

@bigcommerce/catalyst-core@1.3.6

11 Dec 23:45
471a04b

Choose a tag to compare

Patch Changes

  • #2762 7f3a184 Thanks @chanceaclark! - # Next.js 15.5.8 Upgrade

    Catalyst has been upgraded to Next.js 15.5.8. This is a patch version upgrade that requires migration steps for existing stores to fix a security vulnerability.

    🔒 Critical Security Update

    This upgrade addresses a critical security vulnerability (CVE-2025-55184 + CVE-2025-55183) that affects React Server Components. These vulnerabilities allow a Denial of Service attack and Source Code Exposure attach. This upgrade includes:

    • Next.js 15.5.8 with the security patch
    • React 19.1.3 and React DOM 19.1.3 with the security patch

    All users are strongly encouraged to upgrade immediately.

    Key Changes

    • Next.js 15.5.8: Upgraded from Next.js 15.5.7 to 15.5.8
    • ⚛️ React 19: Upgraded to React 19.1.3 and React DOM 19.1.3

    Migration Guide

    Update Dependencies

    If you're maintaining a custom Catalyst store, update your package.json:

    {
      "dependencies": {
        "next": "15.5.8",
        "react": "19.1.3",
        "react-dom": "19.1.3"
      },
      "devDependencies": {
        "@next/bundle-analyzer": "15.5.8",
        "eslint-config-next": "15.5.8"
      }
    }

    Then run:

    pnpm install