Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions .changeset/afraid-islands-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
"@bigcommerce/catalyst-core": minor
---

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:**
```typescript
<C15TConsentManagerProvider
options={{
mode: 'custom',
consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
endpointHandlers: {
showConsentBanner: () => showConsentBanner(isCookieConsentEnabled),
setConsent,
verifyConsent,
},
}}
>
<ClientSideOptionsProvider scripts={scripts}>
{children}
</ClientSideOptionsProvider>
</C15TConsentManagerProvider>
```

**After:**
```typescript
<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:

```typescript
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:

```typescript
{
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:

```typescript
// 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.

```typescript
<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.
123 changes: 123 additions & 0 deletions .changeset/clean-days-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
"@bigcommerce/catalyst-core": patch
---

Update /login/token route error handling and messaging

## Migration steps

### 1. Add `invalidToken` translation key to the `Auth.Login` namespace:
```json
"invalidToken": "Your login link is invalid or has expired. Please try logging in again.",
```

### 2. In `core/app/[locale]/(default)/(auth)/login/token/[token]/route.ts`, add a `console.error` in the `catch` block to log the error details:
```typescript
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);

// ...
}
```

### 3. In `core/app/[locale]/(default)/(auth)/login/page.tsx`, add `error` prop to searchParams and pass it down into the `SignInSection` component:
```typescript
export default async function Login({ params, searchParams }: Props) {
const { locale } = await params;
const { redirectTo = '/account/orders', error } = await searchParams;

setRequestLocale(locale);

const t = await getTranslations('Auth.Login');
const vanityUrl = buildConfig.get('urls').vanityUrl;
const redirectUrl = new URL(redirectTo, vanityUrl);
const redirectTarget = redirectUrl.pathname + redirectUrl.search;
const tokenErrorMessage = error === 'InvalidToken' ? t('invalidToken') : undefined;

return (
<>
<ForceRefresh />
<SignInSection
action={login.bind(null, { redirectTo: redirectTarget })}
emailLabel={t('email')}
error={tokenErrorMessage}
...
```


### 4. Update `core/vibes/soul/sections/sign-in-section/index.tsx` and add the `error` prop, and pass it down to `SignInForm`:
```typescript
interface Props {
// ... existing props
error?: string;
}

// ...

export function SignInSection({
// ... existing variables
error,
}: Props) {
// ...
<SignInForm
action={action}
emailLabel={emailLabel}
error={error}
```

### 5. Update `core/vibes/soul/sections/sign-in-section/sign-in-form.tsx` to take the error prop and display it in the form errors:

```typescript
interface Props {
// ... existing props
error?: string;
}

export function SignInForm({
// ... existing variables
error,
}: Props) {
// ...
useEffect(() => {
// If the form errors change when an "error" search param is in the URL,
// the search param should be removed to prevent showing stale errors.
if (form.errors) {
const url = new URL(window.location.href);

if (url.searchParams.has('error')) {
url.searchParams.delete('error');
window.history.replaceState({}, '', url.toString());
}
}
}, [form.errors]);

const formErrors = () => {
// Form errors should take precedence over the error prop that is passed in.
// This ensures that the most recent errors are displayed to avoid confusion.
if (form.errors) {
return form.errors;
}

if (error) {
return [error];
}

return [];
};

return (
<form {...getFormProps(form)} action={formAction} className="flex grow flex-col gap-5">
// ...
<SubmitButton>{submitLabel}</SubmitButton>
{formErrors().map((err, index) => (
<FormStatus key={index} type="error">
{err}
</FormStatus>
))}
</form>
);
}
```

### 6. Copy all changes in the `core/tests` directory
5 changes: 5 additions & 0 deletions .changeset/common-queens-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": patch
---

Passes `formButtonLabel` from `Reviews` to `ReviewsEmptyState` (was missing) and sets a default value for `formButtonLabel`
5 changes: 5 additions & 0 deletions .changeset/cute-trees-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

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.
5 changes: 5 additions & 0 deletions .changeset/eleven-bugs-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

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.
7 changes: 7 additions & 0 deletions .changeset/facets-displayName.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@bigcommerce/catalyst-core": minor
---

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.
30 changes: 30 additions & 0 deletions .changeset/five-corners-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@bigcommerce/catalyst-core": patch
---

Remove "Exclusive Offers" field temporarily. Currently, the field is not fully implemented in GraphQL, so it may be misleading to display it on the storefront if it's not actually doing anything when registering a customer.

Once the Register Customer operation takes this field into account, we can display it again.

## Migration

Update `core/app/[locale]/(default)/(auth)/register/page.tsx` and add the function:
```ts
// There is currently a GraphQL gap where the "Exclusive Offers" field isn't accounted for
// during customer registration, so the field should not be shown on the Catalyst storefront until it is hooked up.
function removeExlusiveOffersField(field: Field | Field[]): boolean {
if (Array.isArray(field)) {
// Exclusive offers field will always have ID '25', since it is made upon store creation and is also read-only.
return !field.some((f) => f.id === '25');
}

return field.id !== '25';
}
```

Then, add the following code at the end of the `const fields` declaration:
```ts
})
.filter(exists)
.filter(removeExlusiveOffersField); // <---
```
5 changes: 5 additions & 0 deletions .changeset/funny-dingos-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bigcommerce/catalyst-core': minor
---

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.
74 changes: 74 additions & 0 deletions .changeset/hot-plums-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
"@bigcommerce/catalyst-core": minor
---

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`:

```typescript
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:

```typescript
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:**
```typescript
<Subscribe />
```

**After:**
```typescript
<Stream fallback={null} value={streamableShowNewsletterSignup}>
{(showNewsletterSignup) => showNewsletterSignup && <Subscribe />}
</Stream>
```
Loading