Skip to content

Releases: bigcommerce/catalyst

@bigcommerce/catalyst-makeswift@1.6.0

Choose a tag to compare

@github-actions github-actions released this 16 Mar 21:57
cd3d9ac

Minor Changes

  • Pulls in changes from the @bigcommerce/catalyst-core@1.6.0 release. For more information about what was included in the @bigcommerce/catalyst-core@1.6.0 release, see the changelog entry.

@bigcommerce/catalyst-core@1.6.0

Choose a tag to compare

@github-actions github-actions released this 16 Mar 21:23
a00b864

Minor Changes

  • #2896 fc84210 Thanks @jamesqquick! - Add reCAPTCHA v2 support to storefront forms. The reCAPTCHA widget is rendered on the registration, contact, and product review forms when enabled in the BigCommerce admin. All validation and error handling is performed server-side in the corresponding form actions. The token is read from the native g-recaptcha-response field that the widget injects into the form, eliminating the need for manual token extraction on the client.

    Migration steps

    Step 1: Install dependencies

    Add react-google-recaptcha and its type definitions:

    pnpm add react-google-recaptcha
    pnpm add -D @types/react-google-recaptcha

    Step 2: Add the reCAPTCHA server library

    Create core/lib/recaptcha/constants.ts:

    export interface ReCaptchaSettings {
      isEnabledOnStorefront: boolean;
      siteKey: string;
    }
    
    export const RECAPTCHA_TOKEN_FORM_KEY = 'g-recaptcha-response';

    Create core/lib/recaptcha.ts with the server-side helpers for fetching reCAPTCHA settings, extracting the token from form data, and asserting the token is present. See the file in this release for the full implementation.

    Step 3: Add reCAPTCHA translation strings

    Update core/messages/en.json to add the recaptchaRequired message in each form namespace:

      "Auth": {
        "Register": {
    +     "recaptchaRequired": "Please complete the reCAPTCHA verification.",
      "Product": {
        "Reviews": {
          "Form": {
    +       "recaptchaRequired": "Please complete the reCAPTCHA verification.",
      "WebPages": {
        "ContactUs": {
          "Form": {
    +       "recaptchaRequired": "Please complete the reCAPTCHA verification.",
      "Form": {
    +   "recaptchaRequired": "Please complete the reCAPTCHA verification.",

    Step 4: Update GraphQL mutations to accept reCAPTCHA token

    Update core/app/[locale]/(default)/(auth)/register/_actions/register-customer.ts:

    + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';
      ...
      const RegisterCustomerMutation = graphql(`
    -   mutation RegisterCustomerMutation($input: RegisterCustomerInput!) {
    +   mutation RegisterCustomerMutation(
    +     $input: RegisterCustomerInput!
    +     $reCaptchaV2: ReCaptchaV2Input
    +   ) {
          customer {
    -       registerCustomer(input: $input) {
    +       registerCustomer(input: $input, reCaptchaV2: $reCaptchaV2) {

    Update core/app/[locale]/(default)/product/[slug]/_actions/submit-review.ts:

    + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';
      ...
      const AddProductReviewMutation = graphql(`
    -   mutation AddProductReviewMutation($input: AddProductReviewInput!) {
    +   mutation AddProductReviewMutation(
    +     $input: AddProductReviewInput!
    +     $reCaptchaV2: ReCaptchaV2Input
    +   ) {
          catalog {
    -       addProductReview(input: $input) {
    +       addProductReview(input: $input, reCaptchaV2: $reCaptchaV2) {

    Update core/app/[locale]/(default)/webpages/[id]/contact/_actions/submit-contact-form.ts:

    + import { assertRecaptchaTokenPresent, getRecaptchaFromForm } from '~/lib/recaptcha';
      ...
      const SubmitContactUsMutation = graphql(`
    -   mutation SubmitContactUsMutation($input: SubmitContactUsInput!) {
    -     submitContactUs(input: $input) {
    +   mutation SubmitContactUsMutation($input: SubmitContactUsInput!, $reCaptchaV2: ReCaptchaV2Input) {
    +     submitContactUs(input: $input, reCaptchaV2: $reCaptchaV2) {

    Step 5: Add server-side reCAPTCHA validation to form actions

    In each of the three server actions above, add the validation block after the parseWithZod check and pass the token to the GraphQL mutation. For example in register-customer.ts:

    +   const { siteKey, token } = await getRecaptchaFromForm(formData);
    +   const recaptchaValidation = assertRecaptchaTokenPresent(siteKey, token, t('recaptchaRequired'));
    +
    +   if (!recaptchaValidation.success) {
    +     return {
    +       lastResult: submission.reply({ formErrors: recaptchaValidation.formErrors }),
    +     };
    +   }
        ...
        const response = await client.fetch({
          document: RegisterCustomerMutation,
          variables: {
            input,
    +       reCaptchaV2:
    +         recaptchaValidation.token != null ? { token: recaptchaValidation.token } : undefined,
          },

    Apply the same pattern to submit-review.ts and submit-contact-form.ts.

    Step 6: Pass recaptchaSiteKey to form components

    Fetch the site key in each page and pass it down through the component tree.

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

    + import { getRecaptchaSiteKey } from '~/lib/recaptcha';
      ...
    + const recaptchaSiteKey = await getRecaptchaSiteKey();
      ...
      <DynamicFormSection
    +   recaptchaSiteKey={recaptchaSiteKey}

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

    + import { getRecaptchaSiteKey } from '~/lib/recaptcha';
      ...
    - const { product: baseProduct, settings } = await getProduct(productId, customerAccessToken);
    + const [{ product: baseProduct, settings }, recaptchaSiteKey] = await Promise.all([
    +   getProduct(productId, customerAccessToken),
    +   getRecaptchaSiteKey(),
    + ]);
      ...
      <ProductDetail
    +   recaptchaSiteKey={recaptchaSiteKey}
      ...
      <Reviews
    +   recaptchaSiteKey={recaptchaSiteKey}

    Update core/app/[locale]/(default)/webpages/[id]/contact/page.tsx:

    + import { getRecaptchaSiteKey } from '~/lib/recaptcha';
      ...
    + const recaptchaSiteKey = await getRecaptchaSiteKey();
      ...
      <DynamicForm
    +   recaptchaSiteKey={recaptchaSiteKey}

    Step 7: Render the reCAPTCHA widget in form components

    Update core/vibes/soul/form/dynamic-form/index.tsx:

    + import RecaptchaWidget from 'react-google-recaptcha';
      ...
      export interface DynamicFormProps<F extends Field> {
    +   recaptchaSiteKey?: string;
      }
      ...
    +         {recaptchaSiteKey ? <RecaptchaWidget sitekey={recaptchaSiteKey} /> : null}

    Update core/vibes/soul/sections/reviews/review-form.tsx:

    + import RecaptchaWidget from 'react-google-recaptcha';
      ...
      interface Props {
    +   recaptchaSiteKey?: string;
      }
      ...
    +           {recaptchaSiteKey ? (
    +             <div>
    +               <RecaptchaWidget sitekey={recaptchaSiteKey} />
    +             </div>
    +           ) : null}

    Step 8: Thread recaptchaSiteKey through intermediate components

    Add the recaptchaSiteKey?: string prop and pass it through in:

    • core/vibes/soul/sections/dynamic-form-section/index.tsx
    • core/vibes/soul/sections/product-detail/index.tsx
    • core/vibes/soul/sections/reviews/index.tsx
    • core/app/[locale]/(default)/product/[slug]/_components/reviews.tsx

    Each of these accepts the prop and forwards it to the form component that renders the widget.

Patch Changes

@bigcommerce/catalyst-makeswift@1.5.0

Choose a tag to compare

@github-actions github-actions released this 13 Mar 18:28
990727b

Minor Changes

  • Pulls in changes from the @bigcommerce/catalyst-core@1.5.0 release. For more information about what was included in the @bigcommerce/catalyst-core@1.5.0 release, see the changelog entry.

Patch Changes

  • #2919 32be644 Thanks @matthewvolk! - Upgrade @makeswift/runtime to 0.26.3, which uses the <Activity> API instead of <Suspense>. This fixes layout shift both when loading a Makeswift page directly and when client-side navigating from a non-Makeswift page to a Makeswift page after a hard refresh.

  • #2862 52207b6 Thanks @Codeseph! - fix: handle OPTIONS requests via MakeswiftApiHandler

  • #2860 5097034 Thanks @jorgemoya! - Add explicit Makeswift SEO metadata support to public-facing pages. When configured in Makeswift, the SEO title and description will take priority over the default values from BigCommerce or static translations.

    The following pages now support Makeswift SEO metadata:

    • Home page (/)
    • Catch-all page (/[...rest])
    • Product page (/product/[slug])
    • Brand page (/brand/[slug])
    • Category page (/category/[slug])
    • Blog list page (/blog)
    • Blog post page (/blog/[blogId])
    • Search page (/search)
    • Cart page (/cart)
    • Compare page (/compare)
    • Gift certificates page (/gift-certificates)
    • Gift certificates balance page (/gift-certificates/balance)
    • Contact webpage (/webpages/[id]/contact)
    • Normal webpage (/webpages/[id]/normal)

    Migration steps

    Step 1: Add getMakeswiftPageMetadata function

    Add the getMakeswiftPageMetadata function to core/lib/makeswift/client.ts:

    + export async function getMakeswiftPageMetadata({ path, locale }: { path: string; locale: string }) {
    +   const { data: pages } = await client.getPages({
    +     pathPrefix: path,
    +     locale: normalizeLocale(locale),
    +     siteVersion: await getSiteVersion(),
    +   });
    +
    +   if (pages.length === 0 || !pages[0]) {
    +     return null;
    +   }
    +
    +   const { title, description } = pages[0];
    +
    +   return {
    +     ...(title && { title }),
    +     ...(description && { description }),
    +   };
    + }

    Export the function from core/lib/makeswift/index.ts:

      export { Page } from './page';
    - export { client } from './client';
    + export { client, getMakeswiftPageMetadata } from './client';

    Step 2: Update page metadata

    Each page's generateMetadata function has been updated to fetch Makeswift metadata and use it as the primary source, falling back to existing values. Here's an example using the cart page:

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

      import { getPreferredCurrencyCode } from '~/lib/currency';
    + import { getMakeswiftPageMetadata } from '~/lib/makeswift';
      import { Slot } from '~/lib/makeswift/slot';
      export async function generateMetadata({ params }: Props): Promise<Metadata> {
        const { locale } = await params;
    
        const t = await getTranslations({ locale, namespace: 'Cart' });
    +   const makeswiftMetadata = await getMakeswiftPageMetadata({ path: '/cart', locale });
    
        return {
    -     title: t('title'),
    +     title: makeswiftMetadata?.title || t('title'),
    +     description: makeswiftMetadata?.description || undefined,
        };
      }

    Apply the same pattern to the other pages listed above, using the appropriate path for each page (e.g., /blog, /search, /compare, etc.).

@bigcommerce/catalyst-core@1.5.0

Choose a tag to compare

@github-actions github-actions released this 13 Mar 15:38
0d951ab

Minor Changes

  • #2905 6f788e9 Thanks @matthewvolk! - Use dynamic imports for next/headers, next/navigation, and next-intl/server in the client module to avoid AsyncLocalStorage poisoning during next.config.ts resolution

  • #2801 18cfdc8 Thanks @Tharaae! - Fetch product inventory data with a separate GQL query with no caching

    Migration

    The files to be rebased for this change to be applied are:

    • core/app/[locale]/(default)/product/[slug]/page-data.ts
    • core/app/[locale]/(default)/product/[slug]/page.tsx
  • #2863 6a23c90 Thanks @jorgemoya! - Add pagination support for the product gallery. When a product has more images than the initial page load, new images will load as batches once the user reaches the end of the existing thumbnails. Thumbnail images now will display in horizontal direction in all viewport sizes.

    Migration

    1. Create the new server action file core/app/[locale]/(default)/product/[slug]/_actions/get-more-images.ts with a GraphQL query to fetch additional product images with pagination.
    2. Update the product page data fetching in core/app/[locale]/(default)/product/[slug]/page-data.ts to include pageInfo (with hasNextPage and endCursor) from the images query.
    3. Update core/app/[locale]/(default)/product/[slug]/page.tsx to pass the new pagination props (pageInfo, productId, loadMoreAction) to the ProductDetail component.
    4. The ProductGallery component now accepts optional props for pagination:
      • pageInfo?: { hasNextPage: boolean; endCursor: string | null }
      • productId?: number
      • loadMoreAction?: ProductGalleryLoadMoreAction

    Due to the number of changes, it is recommended to use the PR as a reference for migration.

  • #2758 d78bc85 Thanks @Tharaae! - Add the following messages to each line item on cart page based on store inventory settings:

    • Fully/partially out-of-stock message if enabled on the store and the line item is currently out of stock
    • Ready-to-ship quantity if enabled on the store
    • Backordered quantity if enabled on the store

    Migration

    For existing Catalyst stores, to get the newly added feature, simply rebase the existing code with the new release code. The files to be rebased for this change to be applied are:

    • core/app/[locale]/(default)/cart/page-data.ts
    • core/app/[locale]/(default)/cart/page.tsx
    • core/messages/en.json
    • core/vibes/soul/sections/cart/client.tsx
  • #2907 35adccb Thanks @matthewvolk! - Upgrade Next.js to v16 and align peer dependencies.

    To migrate your Catalyst storefront to Next.js 16:

    • Update next to ^16.0.0 in your package.json and install dependencies.
    • Replace any usage of unstable_expireTag with revalidateTag and unstable_expirePath with revalidatePath from next/cache.
    • Update tsconfig.json to use "moduleResolution": "bundler" and "module": "nodenext" as required by Next.js 16.
    • Address Next.js 16 deprecation lint errors (e.g. legacy <img> elements, missing rel="noopener noreferrer" on external links).
    • Rename middleware.ts to proxy.ts and change export const middleware to export const proxy (Next.js 16 proxy pattern).
    • Ensure you are running Node.js 24+ (proxy runs on the Node.js runtime, not Edge).

Patch Changes

  • #2916 e3185b6 Thanks @chanceaclark! - Fix analytics visit count inflation by implementing a sliding window for the visit cookie TTL, guarding against prefetch/RSC requests creating spurious visits, and reordering middleware so analytics cookies survive locale redirects.

  • #2852 a7395f1 Thanks @chanceaclark! - Uses regular dompurify (DP) instead of isomorphic-dompurify (IDP), because IDP requires JSDOM. JSDOM doesn't work in edge-runtime environments even with nodejs compatibility. We only need it on the client anyways for the JSON-LD schema, so it doesn't need the isomorphic aspect of it. This also changes core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx to be a client-component to enable `dompurify to work correctly.

    Migration

    1. Remove the old dependency and add the new:
    pnpm rm isomorphic-dompurify
    pnpm add dompurify -S
    1. Change the import in core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx:
    - import DOMPurify from 'isomorphic-dompurify';
    +// eslint-disable-next-line import/no-named-as-default
    +import DOMPurify from 'dompurify';
    1. Add the 'use client'; directive to the top of core/app/[locale]/(default)/product/[slug]/_components/product-review-schema/product-review-schema.tsx.
  • #2844 74dee6e Thanks @jordanarldt! - Update forms to translate the form field validation errors

    Migration

    Due to the amount of changes, it is recommended to just use the PR as a reference for migration.

    Detailed migration steps can be found on the PR here:
    #2844

  • #2901 8b5fee6 Thanks @jordanarldt! - Fix GiftCertificateCard not updating when selecting a new amount on the gift certificate purchase form

  • #2858 0633612 Thanks @jorgemoya! - Use state abbreviation instead of entityId for cart shipping form state values. The shipping API expects state abbreviations, and using entityId caused form submissions to fail. Additionally, certain US military states that share the same abbreviation (AE) are now filtered out to prevent duplicate key issues and ambiguous submissions.

    Migration steps

    Step 1: Add blacklist for states with duplicate abbreviations

    Certain US states share the same abbreviation (AE), which causes issues with the shipping API and React select dropdowns. Add a blacklist to filter these out.

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

      const countries = shippingCountries.map((country) => ({
        value: country.code,
        label: country.name,
      }));
    
    + // These US states share the same abbreviation (AE), which causes issues:
    + // 1. The shipping API uses abbreviations, so it can't distinguish between them
    + // 2. React select dropdowns require unique keys, causing duplicate key warnings
    + const blacklistedUSStates = new Set([
    +   'Armed Forces Africa',
    +   'Armed Forces Canada',
    +   'Armed Forces Middle East',
    + ]);
    
      const statesOrProvinces = shippingCountries.map((country) => ({

    Step 2: Use state abbreviation instead of entityId

    Update the state mapping to use abbreviation instead of entityId, and apply the blacklist filter for US states.

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

      const statesOrProvinces = shippingCountries.map((country) => ({
        country: country.code,
    -   states: country.statesOrProvinces.map((state) => ({
    -     value: state.entityId.toString(),
    -     label: state.name,
    -   })),
    +   states: country.statesOrProvinces
    +     .filter((state) => country.code !== 'US' || !blacklistedUSStates.has(state.name))
    +     .map((state) => ({
    +       value: state.abbreviation,
    +       label: state.name,
    +     })),
      }));
  • #2856 f5330c7 Thanks @jorgemoya! - Add canonical URLs and hreflang alternates for SEO. Pages now set alternates.canonical and alternates.languages in generateMetadata via the new getMetadataAlternates helper in core/lib/seo/canonical.ts. The helper fetches the vanity URL via GraphQL (site.settings.url.vanityUrl) and is cached per request. The default locale uses no path prefix; other locales use /{locale}/path. The root locale layout sets metadataBase to the configured vanity URL so canonical URLs resolve correctly. On Vercel preview deployments (VERCEL_ENV=preview), metadataBase and canonical/hreflang URLs...

Read more

@bigcommerce/catalyst-makeswift@1.4.2

Choose a tag to compare

@github-actions github-actions released this 27 Jan 21:14
5a33677

Patch Changes

@bigcommerce/catalyst-makeswift@1.4.1

Choose a tag to compare

@github-actions github-actions released this 27 Jan 17:39
19b7991

Patch Changes

@bigcommerce/catalyst-core@1.4.2

Choose a tag to compare

@github-actions github-actions released this 27 Jan 19:06
b435d3c

Patch Changes

@bigcommerce/catalyst-core@1.4.1

Choose a tag to compare

@github-actions github-actions released this 26 Jan 22:19
81b13e4

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

Choose a tag to compare

@github-actions github-actions released this 05 Jan 23:21
46cf4bd

Minor Changes

Patch Changes

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

@bigcommerce/catalyst-core@1.4.0

Choose a tag to compare

@github-actions github-actions released this 05 Jan 22:39
44c682e

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