Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a48bcf2
feat: CATALYST-676 add generic otel setup (#2674)
chanceaclark Nov 13, 2025
edbd202
refactor(auth): separate first and last name fields on user session (…
matthewvolk Nov 13, 2025
b7ba003
feat(reviews): add reviews form enabling shoppers to submit reviews (…
matthewvolk Nov 13, 2025
d1d1249
test(reviews): add e2e tests for reviews form (#2686)
matthewvolk Nov 13, 2025
7899bbd
fix(core): handle inline scripts with src attribute (#2692)
chanceaclark Nov 14, 2025
b0174dd
chore: merge 1.3.2 changelog into canary (#2693)
chanceaclark Nov 14, 2025
b7000ba
Revert minor changes (#2701)
jordanarldt Nov 14, 2025
fdedcaa
Update anonymous-session.ts (#2694)
BC-AdamWard Nov 14, 2025
6d565c2
feat(core): Add link to Gift Certificates page in header/footer (#2695)
jordanarldt Nov 14, 2025
f29d480
refactor(auth): separate first and last name fields on user session (…
jordanarldt Nov 14, 2025
99b9a3f
feat: CATALYST-676 add generic otel setup (#2705)
jordanarldt Nov 14, 2025
54ddf5a
feat(reviews): add reviews form enabling shoppers to submit reviews (…
jordanarldt Nov 14, 2025
de20ec4
test(reviews): add e2e tests for reviews form (#2702)
jordanarldt Nov 14, 2025
5c7aef9
Revert "feat: CATALYST-676 add generic otel setup (#2705)" (#2707)
jordanarldt Nov 14, 2025
45d7b70
Revert "feat(reviews): add reviews form enabling shoppers to submit r…
jordanarldt Nov 14, 2025
90b20ca
Revert "refactor(auth): separate first and last name fields on user s…
jordanarldt Nov 14, 2025
dbbb920
Version Packages (`canary`) (#2689)
github-actions[bot] Nov 14, 2025
04ebcb6
Merge branch 'canary' into sync-integrations-makeswift
jordanarldt Nov 14, 2025
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
8 changes: 8 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 1.3.3

### Patch Changes

- [#2695](https://github.com/bigcommerce/catalyst/pull/2695) [`6d565c2`](https://github.com/bigcommerce/catalyst/commit/6d565c2cbf98e3fa0c2b0142734fc68a5d48bd2c) Thanks [@jordanarldt](https://github.com/jordanarldt)! - Add Gift Certificates link to the header/footer.

- [#2694](https://github.com/bigcommerce/catalyst/pull/2694) [`fdedcaa`](https://github.com/bigcommerce/catalyst/commit/fdedcaa99c83d5c32c54dec0974962b7d17447cf) Thanks [@BC-AdamWard](https://github.com/BC-AdamWard)! - Fix anonymous session cookie maxAge calculation to correctly set 7 days instead of 7 hours.

## 1.3.2

### Patch Changes
Expand Down
15 changes: 13 additions & 2 deletions core/app/[locale]/(default)/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,27 @@ export const LayoutQuery = graphql(
[HeaderFragment, FooterFragment],
);

const GiftCertificatesEnabledFragment = graphql(`
fragment GiftCertificatesEnabledFragment on Settings {
giftCertificates(currencyCode: $currencyCode) {
isEnabled
}
}
`);

export const GetLinksAndSectionsQuery = graphql(
`
query GetLinksAndSectionsQuery {
query GetLinksAndSectionsQuery($currencyCode: currencyCode) {
site {
settings {
...GiftCertificatesEnabledFragment
}
...HeaderLinksFragment
...FooterSectionsFragment
}
}
`,
[HeaderLinksFragment, FooterSectionsFragment],
[HeaderLinksFragment, FooterSectionsFragment, GiftCertificatesEnabledFragment],
);

const HomePageQuery = graphql(
Expand Down
2 changes: 1 addition & 1 deletion core/auth/anonymous-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const anonymousSignIn = async (user: Partial<AnonymousUser> = { cartId: n
sameSite: 'lax',
// We set the maxAge to 7 days as a good default for anonymous sessions.
// This can be adjusted based on your application's needs.
maxAge: 60 * 60 * 7, // 7 days
maxAge: 60 * 60 * 24 * 7, // 7 days
httpOnly: true,
});
};
Expand Down
6 changes: 6 additions & 0 deletions core/components/footer/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const FooterFragment = graphql(`

export const FooterSectionsFragment = graphql(`
fragment FooterSectionsFragment on Site {
settings {
giftCertificates(currencyCode: $currencyCode) {
currencyCode
isEnabled
}
}
content {
pages(filters: { parentEntityIds: [0] }) {
edges {
Expand Down
52 changes: 33 additions & 19 deletions core/components/footer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import { client } from '~/client';
import { readFragment } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { CurrencyCode } from '~/components/header/fragment';
import { logoTransformer } from '~/data-transformers/logo-transformer';
import { SiteFooter as FooterSection } from '~/lib/makeswift/components/site-footer';
import { getPreferredCurrencyCode } from '~/lib/currency';

Check warning on line 21 in core/components/footer/index.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

`~/lib/currency` import should occur before import of `~/lib/makeswift/components/site-footer`

import { FooterFragment, FooterSectionsFragment } from './fragment';
import { AmazonIcon } from './payment-icons/amazon';
Expand Down Expand Up @@ -44,18 +46,21 @@
YouTube: { icon: <SiYoutube title="YouTube" /> },
};

const getFooterSections = cache(async (customerAccessToken?: string) => {
const { data: response } = await client.fetch({
document: GetLinksAndSectionsQuery,
customerAccessToken,
// Since this query is needed on every page, it's a good idea not to validate the customer access token.
// The 'cache' function also caches errors, so we might get caught in a redirect loop if the cache saves an invalid token error response.
validateCustomerAccessToken: false,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
});

return readFragment(FooterSectionsFragment, response).site;
});
const getFooterSections = cache(
async (customerAccessToken?: string, currencyCode?: CurrencyCode) => {
const { data: response } = await client.fetch({
document: GetLinksAndSectionsQuery,
customerAccessToken,
variables: { currencyCode },
// Since this query is needed on every page, it's a good idea not to validate the customer access token.
// The 'cache' function also caches errors, so we might get caught in a redirect loop if the cache saves an invalid token error response.
validateCustomerAccessToken: false,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
});

return readFragment(FooterSectionsFragment, response).site;
},
);

const getFooterData = cache(async () => {
const { data: response } = await client.fetch({
Expand All @@ -68,7 +73,6 @@

export const Footer = async () => {
const t = await getTranslations('Components.Footer');

const data = await getFooterData();

const logo = data.settings ? logoTransformer(data.settings) : '';
Expand All @@ -91,8 +95,8 @@

const streamableSections = Streamable.from(async () => {
const customerAccessToken = await getSessionCustomerAccessToken();

const sectionsData = await getFooterSections(customerAccessToken);
const currencyCode = await getPreferredCurrencyCode();
const sectionsData = await getFooterSections(customerAccessToken, currencyCode);

return [
{
Expand All @@ -111,10 +115,20 @@
},
{
title: t('navigate'),
links: removeEdgesAndNodes(sectionsData.content.pages).map((page) => ({
label: page.name,
href: page.__typename === 'ExternalLinkPage' ? page.link : page.path,
})),
links: [
...(sectionsData.settings?.giftCertificates?.isEnabled
? [
{
label: t('giftCertificates'),
href: '/gift-certificates',
},
]
: []),
...removeEdgesAndNodes(sectionsData.content.pages).map((page) => ({
label: page.name,
href: page.__typename === 'ExternalLinkPage' ? page.link : page.path,
})),
],
},
];
});
Expand Down
31 changes: 25 additions & 6 deletions core/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { SiteHeader as HeaderSection } from '~/lib/makeswift/components/site-hea

import { search } from './_actions/search';
import { switchCurrency } from './_actions/switch-currency';
import { HeaderFragment, HeaderLinksFragment } from './fragment';
import { CurrencyCode, HeaderFragment, HeaderLinksFragment } from './fragment';

const GetCartCountQuery = graphql(`
query GetCartCountQuery($cartId: String) {
Expand Down Expand Up @@ -47,17 +47,18 @@ const getCartCount = cache(async (cartId: string, customerAccessToken?: string)
return response.data.site.cart?.lineItems.totalQuantity ?? null;
});

const getHeaderLinks = cache(async (customerAccessToken?: string) => {
const getHeaderLinks = cache(async (customerAccessToken?: string, currencyCode?: CurrencyCode) => {
const { data: response } = await client.fetch({
document: GetLinksAndSectionsQuery,
customerAccessToken,
variables: { currencyCode },
// Since this query is needed on every page, it's a good idea not to validate the customer access token.
// The 'cache' function also caches errors, so we might get caught in a redirect loop if the cache saves an invalid token error response.
validateCustomerAccessToken: false,
fetchOptions: customerAccessToken ? { cache: 'no-store' } : { next: { revalidate } },
});

return readFragment(HeaderLinksFragment, response).site.categoryTree;
return readFragment(HeaderLinksFragment, response).site;
});

const getHeaderData = cache(async () => {
Expand Down Expand Up @@ -94,9 +95,13 @@ export const Header = async () => {
: [];

const streamableLinks = Streamable.from(async () => {
const customerAccessToken = await getSessionCustomerAccessToken();

const categoryTree = await getHeaderLinks(customerAccessToken);
const [customerAccessToken, currencyCode] = await Promise.all([
getSessionCustomerAccessToken(),
getPreferredCurrencyCode(),
]);
// const customerAccessToken = await getSessionCustomerAccessToken();
// const currencyCode = await getPreferredCurrencyCode();
const categoryTree = (await getHeaderLinks(customerAccessToken, currencyCode)).categoryTree;

/** To prevent the navigation menu from overflowing, we limit the number of categories to 6.
To show a full list of categories, modify the `slice` method to remove the limit.
Expand All @@ -118,6 +123,17 @@ export const Header = async () => {
}));
});

const streamableGiftCertificatesEnabled = Streamable.from(async () => {
const [customerAccessToken, currencyCode] = await Promise.all([
getSessionCustomerAccessToken(),
getPreferredCurrencyCode(),
]);
const giftCertificateSettings = (await getHeaderLinks(customerAccessToken, currencyCode))
.settings?.giftCertificates;

return giftCertificateSettings?.isEnabled ?? false;
});

const streamableCartCount = Streamable.from(async () => {
const cartId = await getCartId();
const customerAccessToken = await getSessionCustomerAccessToken();
Expand All @@ -144,6 +160,9 @@ export const Header = async () => {
accountLabel: t('Icons.account'),
cartHref: '/cart',
cartLabel: t('Icons.cart'),
giftCertificatesLabel: t('Icons.giftCertificates'),
giftCertificatesHref: '/gift-certificates',
giftCertificatesEnabled: streamableGiftCertificatesEnabled,
searchHref: '/search',
searchParamName: 'term',
searchAction: search,
Expand Down
6 changes: 4 additions & 2 deletions core/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,8 @@
"Icons": {
"account": "Profile",
"cart": "Cart",
"search": "Open search popup"
"search": "Open search popup",
"giftCertificates": "Gift certificates"
},
"SwitchCurrency": {
"label": "Switch currency",
Expand All @@ -538,7 +539,8 @@
"socialMediaLinks": "Social media links",
"categories": "Categories",
"brands": "Brands",
"navigate": "Navigate"
"navigate": "Navigate",
"giftCertificates": "Gift certificates"
},
"Subscribe": {
"title": "Sign up for our newsletter",
Expand Down
96 changes: 96 additions & 0 deletions core/tests/ui/e2e/reviews.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { faker } from '@faker-js/faker';

import { expect, test } from '~/tests/fixtures';
import { getTranslations } from '~/tests/lib/i18n';
import { TAGS } from '~/tests/tags';

test(
'Submit a review as a non-logged in customer',
{ tag: [TAGS.writesData] },
async ({ page, catalog }) => {
const t = await getTranslations('Product.Reviews.Form');
const product = await catalog.getDefaultOrCreateSimpleProduct();

await page.goto(product.path);
await page.waitForLoadState('networkidle');

await page.getByRole('button', { name: t('button') }).click();

const modal = page.getByRole('dialog');

await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();

const rating = faker.number.int({ min: 1, max: 5 });
const ratingLabel = rating === 1 ? '1 star' : `${rating} stars`;

await modal.getByLabel(ratingLabel).click();

const reviewTitle = faker.lorem.sentence();

await modal.getByLabel(t('titleLabel')).fill(reviewTitle);

const reviewText = faker.lorem.paragraph();

await modal.getByLabel(t('reviewLabel')).fill(reviewText);

const customerName = faker.person.fullName();

await modal.getByLabel(t('nameLabel')).fill(customerName);

const customerEmail = faker.internet.email();

await modal.getByLabel(t('emailLabel')).fill(customerEmail);

await expect(modal.getByLabel(t('nameLabel'))).toBeEnabled();
await expect(modal.getByLabel(t('emailLabel'))).toBeEnabled();

await modal.getByRole('button', { name: t('submit') }).click();
await page.waitForLoadState('networkidle');

await expect(page.getByText(t('successMessage'))).toBeVisible();
await expect(modal).toBeHidden();
await expect(page.getByRole('button', { name: t('button') })).toBeVisible();
},
);

test('Shows validation errors when submitting review form with empty inputs', async ({
page,
catalog,
}) => {
const t = await getTranslations('Product.Reviews.Form');
const product = await catalog.getDefaultOrCreateSimpleProduct();

await page.goto(product.path);
await page.waitForLoadState('networkidle');

await page.getByRole('button', { name: t('button') }).click();

const modal = page.getByRole('dialog');

await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();

await modal.getByRole('button', { name: t('submit') }).click();
await page.waitForLoadState('networkidle');

await expect(modal).toBeVisible();

const ratingField = modal.getByLabel(t('ratingLabel'));
const titleField = modal.getByLabel(t('titleLabel'));
const reviewField = modal.getByLabel(t('reviewLabel'));
const nameField = modal.getByLabel(t('nameLabel'));
const emailField = modal.getByLabel(t('emailLabel'));

await expect(ratingField).toBeVisible();
await expect(titleField).toBeVisible();
await expect(reviewField).toBeVisible();
await expect(nameField).toBeVisible();
await expect(emailField).toBeVisible();

const errorMessages = modal.locator('text=/required|invalid/i');

await expect(errorMessages.first()).toBeVisible();
await expect(errorMessages.nth(1)).toBeVisible();
await expect(errorMessages.nth(2)).toBeVisible();
await expect(errorMessages.nth(3)).toBeVisible();
await expect(errorMessages.nth(4)).toBeVisible();
});
Loading
Loading