Skip to content

Commit 04ebcb6

Browse files
committed
Merge branch 'canary' into sync-integrations-makeswift
2 parents c37cce9 + dbbb920 commit 04ebcb6

9 files changed

Lines changed: 215 additions & 31 deletions

File tree

core/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 1.3.3
4+
5+
### Patch Changes
6+
7+
- [#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.
8+
9+
- [#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.
10+
311
## 1.3.2
412

513
### Patch Changes

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,27 @@ export const LayoutQuery = graphql(
2020
[HeaderFragment, FooterFragment],
2121
);
2222

23+
const GiftCertificatesEnabledFragment = graphql(`
24+
fragment GiftCertificatesEnabledFragment on Settings {
25+
giftCertificates(currencyCode: $currencyCode) {
26+
isEnabled
27+
}
28+
}
29+
`);
30+
2331
export const GetLinksAndSectionsQuery = graphql(
2432
`
25-
query GetLinksAndSectionsQuery {
33+
query GetLinksAndSectionsQuery($currencyCode: currencyCode) {
2634
site {
35+
settings {
36+
...GiftCertificatesEnabledFragment
37+
}
2738
...HeaderLinksFragment
2839
...FooterSectionsFragment
2940
}
3041
}
3142
`,
32-
[HeaderLinksFragment, FooterSectionsFragment],
43+
[HeaderLinksFragment, FooterSectionsFragment, GiftCertificatesEnabledFragment],
3344
);
3445

3546
const HomePageQuery = graphql(

core/auth/anonymous-session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const anonymousSignIn = async (user: Partial<AnonymousUser> = { cartId: n
3333
sameSite: 'lax',
3434
// We set the maxAge to 7 days as a good default for anonymous sessions.
3535
// This can be adjusted based on your application's needs.
36-
maxAge: 60 * 60 * 7, // 7 days
36+
maxAge: 60 * 60 * 24 * 7, // 7 days
3737
httpOnly: true,
3838
});
3939
};

core/components/footer/fragment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export const FooterFragment = graphql(`
3030

3131
export const FooterSectionsFragment = graphql(`
3232
fragment FooterSectionsFragment on Site {
33+
settings {
34+
giftCertificates(currencyCode: $currencyCode) {
35+
currencyCode
36+
isEnabled
37+
}
38+
}
3339
content {
3440
pages(filters: { parentEntityIds: [0] }) {
3541
edges {

core/components/footer/index.tsx

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import { getSessionCustomerAccessToken } from '~/auth';
1515
import { client } from '~/client';
1616
import { readFragment } from '~/client/graphql';
1717
import { revalidate } from '~/client/revalidate-target';
18+
import { CurrencyCode } from '~/components/header/fragment';
1819
import { logoTransformer } from '~/data-transformers/logo-transformer';
1920
import { SiteFooter as FooterSection } from '~/lib/makeswift/components/site-footer';
21+
import { getPreferredCurrencyCode } from '~/lib/currency';
2022

2123
import { FooterFragment, FooterSectionsFragment } from './fragment';
2224
import { AmazonIcon } from './payment-icons/amazon';
@@ -44,18 +46,21 @@ const socialIcons: Record<string, { icon: JSX.Element }> = {
4446
YouTube: { icon: <SiYoutube title="YouTube" /> },
4547
};
4648

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

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

6974
export const Footer = async () => {
7075
const t = await getTranslations('Components.Footer');
71-
7276
const data = await getFooterData();
7377

7478
const logo = data.settings ? logoTransformer(data.settings) : '';
@@ -91,8 +95,8 @@ export const Footer = async () => {
9195

9296
const streamableSections = Streamable.from(async () => {
9397
const customerAccessToken = await getSessionCustomerAccessToken();
94-
95-
const sectionsData = await getFooterSections(customerAccessToken);
98+
const currencyCode = await getPreferredCurrencyCode();
99+
const sectionsData = await getFooterSections(customerAccessToken, currencyCode);
96100

97101
return [
98102
{
@@ -111,10 +115,20 @@ export const Footer = async () => {
111115
},
112116
{
113117
title: t('navigate'),
114-
links: removeEdgesAndNodes(sectionsData.content.pages).map((page) => ({
115-
label: page.name,
116-
href: page.__typename === 'ExternalLinkPage' ? page.link : page.path,
117-
})),
118+
links: [
119+
...(sectionsData.settings?.giftCertificates?.isEnabled
120+
? [
121+
{
122+
label: t('giftCertificates'),
123+
href: '/gift-certificates',
124+
},
125+
]
126+
: []),
127+
...removeEdgesAndNodes(sectionsData.content.pages).map((page) => ({
128+
label: page.name,
129+
href: page.__typename === 'ExternalLinkPage' ? page.link : page.path,
130+
})),
131+
],
118132
},
119133
];
120134
});

core/components/header/index.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { SiteHeader as HeaderSection } from '~/lib/makeswift/components/site-hea
1616

1717
import { search } from './_actions/search';
1818
import { switchCurrency } from './_actions/switch-currency';
19-
import { HeaderFragment, HeaderLinksFragment } from './fragment';
19+
import { CurrencyCode, HeaderFragment, HeaderLinksFragment } from './fragment';
2020

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

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

60-
return readFragment(HeaderLinksFragment, response).site.categoryTree;
61+
return readFragment(HeaderLinksFragment, response).site;
6162
});
6263

6364
const getHeaderData = cache(async () => {
@@ -94,9 +95,13 @@ export const Header = async () => {
9495
: [];
9596

9697
const streamableLinks = Streamable.from(async () => {
97-
const customerAccessToken = await getSessionCustomerAccessToken();
98-
99-
const categoryTree = await getHeaderLinks(customerAccessToken);
98+
const [customerAccessToken, currencyCode] = await Promise.all([
99+
getSessionCustomerAccessToken(),
100+
getPreferredCurrencyCode(),
101+
]);
102+
// const customerAccessToken = await getSessionCustomerAccessToken();
103+
// const currencyCode = await getPreferredCurrencyCode();
104+
const categoryTree = (await getHeaderLinks(customerAccessToken, currencyCode)).categoryTree;
100105

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

126+
const streamableGiftCertificatesEnabled = Streamable.from(async () => {
127+
const [customerAccessToken, currencyCode] = await Promise.all([
128+
getSessionCustomerAccessToken(),
129+
getPreferredCurrencyCode(),
130+
]);
131+
const giftCertificateSettings = (await getHeaderLinks(customerAccessToken, currencyCode))
132+
.settings?.giftCertificates;
133+
134+
return giftCertificateSettings?.isEnabled ?? false;
135+
});
136+
121137
const streamableCartCount = Streamable.from(async () => {
122138
const cartId = await getCartId();
123139
const customerAccessToken = await getSessionCustomerAccessToken();
@@ -144,6 +160,9 @@ export const Header = async () => {
144160
accountLabel: t('Icons.account'),
145161
cartHref: '/cart',
146162
cartLabel: t('Icons.cart'),
163+
giftCertificatesLabel: t('Icons.giftCertificates'),
164+
giftCertificatesHref: '/gift-certificates',
165+
giftCertificatesEnabled: streamableGiftCertificatesEnabled,
147166
searchHref: '/search',
148167
searchParamName: 'term',
149168
searchAction: search,

core/messages/en.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,8 @@
514514
"Icons": {
515515
"account": "Profile",
516516
"cart": "Cart",
517-
"search": "Open search popup"
517+
"search": "Open search popup",
518+
"giftCertificates": "Gift certificates"
518519
},
519520
"SwitchCurrency": {
520521
"label": "Switch currency",
@@ -538,7 +539,8 @@
538539
"socialMediaLinks": "Social media links",
539540
"categories": "Categories",
540541
"brands": "Brands",
541-
"navigate": "Navigate"
542+
"navigate": "Navigate",
543+
"giftCertificates": "Gift certificates"
542544
},
543545
"Subscribe": {
544546
"title": "Sign up for our newsletter",

core/tests/ui/e2e/reviews.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { expect, test } from '~/tests/fixtures';
4+
import { getTranslations } from '~/tests/lib/i18n';
5+
import { TAGS } from '~/tests/tags';
6+
7+
test(
8+
'Submit a review as a non-logged in customer',
9+
{ tag: [TAGS.writesData] },
10+
async ({ page, catalog }) => {
11+
const t = await getTranslations('Product.Reviews.Form');
12+
const product = await catalog.getDefaultOrCreateSimpleProduct();
13+
14+
await page.goto(product.path);
15+
await page.waitForLoadState('networkidle');
16+
17+
await page.getByRole('button', { name: t('button') }).click();
18+
19+
const modal = page.getByRole('dialog');
20+
21+
await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();
22+
23+
const rating = faker.number.int({ min: 1, max: 5 });
24+
const ratingLabel = rating === 1 ? '1 star' : `${rating} stars`;
25+
26+
await modal.getByLabel(ratingLabel).click();
27+
28+
const reviewTitle = faker.lorem.sentence();
29+
30+
await modal.getByLabel(t('titleLabel')).fill(reviewTitle);
31+
32+
const reviewText = faker.lorem.paragraph();
33+
34+
await modal.getByLabel(t('reviewLabel')).fill(reviewText);
35+
36+
const customerName = faker.person.fullName();
37+
38+
await modal.getByLabel(t('nameLabel')).fill(customerName);
39+
40+
const customerEmail = faker.internet.email();
41+
42+
await modal.getByLabel(t('emailLabel')).fill(customerEmail);
43+
44+
await expect(modal.getByLabel(t('nameLabel'))).toBeEnabled();
45+
await expect(modal.getByLabel(t('emailLabel'))).toBeEnabled();
46+
47+
await modal.getByRole('button', { name: t('submit') }).click();
48+
await page.waitForLoadState('networkidle');
49+
50+
await expect(page.getByText(t('successMessage'))).toBeVisible();
51+
await expect(modal).toBeHidden();
52+
await expect(page.getByRole('button', { name: t('button') })).toBeVisible();
53+
},
54+
);
55+
56+
test('Shows validation errors when submitting review form with empty inputs', async ({
57+
page,
58+
catalog,
59+
}) => {
60+
const t = await getTranslations('Product.Reviews.Form');
61+
const product = await catalog.getDefaultOrCreateSimpleProduct();
62+
63+
await page.goto(product.path);
64+
await page.waitForLoadState('networkidle');
65+
66+
await page.getByRole('button', { name: t('button') }).click();
67+
68+
const modal = page.getByRole('dialog');
69+
70+
await expect(modal.getByRole('heading', { name: t('title') })).toBeVisible();
71+
72+
await modal.getByRole('button', { name: t('submit') }).click();
73+
await page.waitForLoadState('networkidle');
74+
75+
await expect(modal).toBeVisible();
76+
77+
const ratingField = modal.getByLabel(t('ratingLabel'));
78+
const titleField = modal.getByLabel(t('titleLabel'));
79+
const reviewField = modal.getByLabel(t('reviewLabel'));
80+
const nameField = modal.getByLabel(t('nameLabel'));
81+
const emailField = modal.getByLabel(t('emailLabel'));
82+
83+
await expect(ratingField).toBeVisible();
84+
await expect(titleField).toBeVisible();
85+
await expect(reviewField).toBeVisible();
86+
await expect(nameField).toBeVisible();
87+
await expect(emailField).toBeVisible();
88+
89+
const errorMessages = modal.locator('text=/required|invalid/i');
90+
91+
await expect(errorMessages.first()).toBeVisible();
92+
await expect(errorMessages.nth(1)).toBeVisible();
93+
await expect(errorMessages.nth(2)).toBeVisible();
94+
await expect(errorMessages.nth(3)).toBeVisible();
95+
await expect(errorMessages.nth(4)).toBeVisible();
96+
});

0 commit comments

Comments
 (0)