From 7f0a2a87e6d0eb39ea5565ee368a595e0f8d04eb Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Tue, 2 Jul 2024 14:01:26 -0500 Subject: [PATCH 1/2] feat(core): make fetch channel aware per locale --- .changeset/five-emus-brush.md | 5 ++++ .changeset/smooth-trains-watch.md | 5 ++++ .../(faceted)/brand/[slug]/static/page.tsx | 2 ++ .../(faceted)/category/[slug]/static/page.tsx | 2 ++ core/app/[locale]/(default)/layout.tsx | 4 +-- core/app/[locale]/(default)/page.tsx | 4 +-- .../(default)/product/[slug]/static/page.tsx | 2 ++ core/app/api/cart-quantity/route.ts | 10 ++++--- core/app/api/product/[id]/route.ts | 6 +++++ core/channels.config.ts | 17 ++++++++++++ core/client/index.ts | 27 +++++++++++++++++++ core/client/queries/get-cart.ts | 3 ++- core/client/queries/get-route.ts | 3 ++- core/client/queries/get-store-status.ts | 3 ++- core/components/header/cart-icon.tsx | 6 +++-- .../product-sheet/product-sheet-content.tsx | 10 +++++-- core/middlewares/with-routes.ts | 12 ++++++--- packages/client/src/client.ts | 21 ++++++++++----- 18 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 .changeset/five-emus-brush.md create mode 100644 .changeset/smooth-trains-watch.md create mode 100644 core/channels.config.ts diff --git a/.changeset/five-emus-brush.md b/.changeset/five-emus-brush.md new file mode 100644 index 0000000000..ef46860730 --- /dev/null +++ b/.changeset/five-emus-brush.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +Make client.fetch channel aware per locale. diff --git a/.changeset/smooth-trains-watch.md b/.changeset/smooth-trains-watch.md new file mode 100644 index 0000000000..96f32f4506 --- /dev/null +++ b/.changeset/smooth-trains-watch.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-client": patch +--- + +Add getChannelId param to dynamically fetch a channel on requests. diff --git a/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx b/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx index 8f0516d415..62dd98e156 100644 --- a/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx @@ -1,6 +1,7 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { cache } from 'react'; +import { getChannelIdFromLocale } from '~/channels.config'; import { client } from '~/client'; import { graphql, VariablesOf } from '~/client/graphql'; import { revalidate as revalidateTarget } from '~/client/revalidate-target'; @@ -31,6 +32,7 @@ const getBrands = cache(async (variables: BrandsQueryVariables = {}) => { document: BrandsQuery, variables, fetchOptions: { next: { revalidate: revalidateTarget } }, + channelId: getChannelIdFromLocale(), }); return removeEdgesAndNodes(response.data.site.brands); diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx b/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx index 22bf1062af..93c58ba1a7 100644 --- a/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx @@ -1,5 +1,6 @@ import { cache } from 'react'; +import { getChannelIdFromLocale } from '~/channels.config'; import { client } from '~/client'; import { graphql, VariablesOf } from '~/client/graphql'; import { revalidate as revalidateTarget } from '~/client/revalidate-target'; @@ -29,6 +30,7 @@ const getCategoryTree = cache(async (variables: CategoryTreeQueryVariables = {}) document: CategoryTreeQuery, variables, fetchOptions: { next: { revalidate: revalidateTarget } }, + channelId: getChannelIdFromLocale(), }); return response.data.site.categoryTree; diff --git a/core/app/[locale]/(default)/layout.tsx b/core/app/[locale]/(default)/layout.tsx index f4115fecbc..3c58b2a270 100644 --- a/core/app/[locale]/(default)/layout.tsx +++ b/core/app/[locale]/(default)/layout.tsx @@ -29,6 +29,8 @@ const LayoutQuery = graphql( ); export default async function DefaultLayout({ children, params: { locale } }: Props) { + unstable_setRequestLocale(locale); + const customerId = await getSessionCustomerId(); const { data } = await client.fetch({ @@ -36,8 +38,6 @@ export default async function DefaultLayout({ children, params: { locale } }: Pr fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } }, }); - unstable_setRequestLocale(locale); - const messages = await getMessages({ locale }); return ( diff --git a/core/app/[locale]/(default)/page.tsx b/core/app/[locale]/(default)/page.tsx index e466c10ce8..36ed4d632b 100644 --- a/core/app/[locale]/(default)/page.tsx +++ b/core/app/[locale]/(default)/page.tsx @@ -44,10 +44,10 @@ const HomePageQuery = graphql( ); export default async function Home({ params: { locale } }: Props) { - const customerId = await getSessionCustomerId(); - unstable_setRequestLocale(locale); + const customerId = await getSessionCustomerId(); + const t = await getTranslations({ locale, namespace: 'Home' }); const messages = await getMessages({ locale }); diff --git a/core/app/[locale]/(default)/product/[slug]/static/page.tsx b/core/app/[locale]/(default)/product/[slug]/static/page.tsx index 452d5dbb1d..a9d0814f10 100644 --- a/core/app/[locale]/(default)/product/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/product/[slug]/static/page.tsx @@ -2,6 +2,7 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; import { cache } from 'react'; import { getSessionCustomerId } from '~/auth'; +import { getChannelIdFromLocale } from '~/channels.config'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; import { revalidate as revalidateTarget } from '~/client/revalidate-target'; @@ -38,6 +39,7 @@ const getFeaturedProducts = cache(async ({ first = 12 }: Options = {}) => { variables: { first }, customerId, fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate: revalidateTarget } }, + channelId: getChannelIdFromLocale(), }); return removeEdgesAndNodes(response.data.site.featuredProducts); diff --git a/core/app/api/cart-quantity/route.ts b/core/app/api/cart-quantity/route.ts index c85c632564..f0d7ab901d 100644 --- a/core/app/api/cart-quantity/route.ts +++ b/core/app/api/cart-quantity/route.ts @@ -1,13 +1,17 @@ import { cookies } from 'next/headers'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { getChannelIdFromLocale } from '~/channels.config'; import { getCart } from '~/client/queries/get-cart'; -export const GET = async () => { +export const GET = async (request: NextRequest) => { const cartId = cookies().get('cartId')?.value; + const searchParams = request.nextUrl.searchParams; + const locale = searchParams.get('locale') ?? undefined; + if (cartId) { - const cart = await getCart(cartId); + const cart = await getCart(cartId, getChannelIdFromLocale(locale)); return NextResponse.json({ count: cart?.lineItems.totalQuantity ?? 0 }); } diff --git a/core/app/api/product/[id]/route.ts b/core/app/api/product/[id]/route.ts index 9c265889a7..e8bcaacc9e 100644 --- a/core/app/api/product/[id]/route.ts +++ b/core/app/api/product/[id]/route.ts @@ -3,6 +3,7 @@ */ import { NextRequest, NextResponse } from 'next/server'; +import { getChannelIdFromLocale } from '~/channels.config'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; import { ProductSheetContentFragment } from '~/components/product-sheet/fragment'; @@ -31,6 +32,10 @@ export const GET = async (request: NextRequest, { params }: { params: { id: stri const { id } = params; const searchParams = request.nextUrl.searchParams; + const locale = searchParams.get('locale') ?? undefined; + + searchParams.delete('locale'); + const optionValueIds = Array.from(searchParams.entries(), ([option, value]) => ({ optionEntityId: Number(option), valueEntityId: Number(value), @@ -42,6 +47,7 @@ export const GET = async (request: NextRequest, { params }: { params: { id: stri const { data } = await client.fetch({ document: GetProductQuery, variables: { productId: Number(id), optionValueIds }, + channelId: getChannelIdFromLocale(locale), }); return NextResponse.json(data.site.product); diff --git a/core/channels.config.ts b/core/channels.config.ts new file mode 100644 index 0000000000..b842d80041 --- /dev/null +++ b/core/channels.config.ts @@ -0,0 +1,17 @@ +import { type LocaleType } from './i18n'; + +export type RecordFromLocales = { + [K in LocaleType]: string; +}; + +// Set overrides per locale +const localeToChannelsMappings: Partial = { + // es: '12345', +}; + +function getChannelIdFromLocale(locale?: string) { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return localeToChannelsMappings[locale as LocaleType] ?? process.env.BIGCOMMERCE_CHANNEL_ID; +} + +export { getChannelIdFromLocale }; diff --git a/core/client/index.ts b/core/client/index.ts index 1aaf6e8a0b..ce4d3f0b59 100644 --- a/core/client/index.ts +++ b/core/client/index.ts @@ -1,4 +1,8 @@ import { createClient } from '@bigcommerce/catalyst-client'; +// import { headers } from 'next/headers'; +import { getLocale } from 'next-intl/server'; + +import { getChannelIdFromLocale } from '~/channels.config'; import { backendUserAgent } from '../userAgent'; @@ -11,4 +15,27 @@ export const client = createClient({ logger: (process.env.NODE_ENV !== 'production' && process.env.CLIENT_LOGGER !== 'false') || process.env.CLIENT_LOGGER === 'true', + getChannelId: async (defaultChannelId: string) => { + /** + * Next-intl `getLocale` only works on the server, and when middleware has run. + * + * Instances when `getLocale` will not work: + * - Requests in middlewares + * - Requests in `generateStaticParams` + * - Request in api routes + * - Requests in static sites without `unstable_setRequestLocale` + * + * We use the default channelId as a fallback, but it is not ideal in some scenarios. + * */ + try { + const locale = await getLocale(); + + return getChannelIdFromLocale(locale) ?? defaultChannelId; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error using `getLocale`, using default channel id instead.'); + + return defaultChannelId; + } + }, }); diff --git a/core/client/queries/get-cart.ts b/core/client/queries/get-cart.ts index 6d6483e5d3..c00f8c8aae 100644 --- a/core/client/queries/get-cart.ts +++ b/core/client/queries/get-cart.ts @@ -121,7 +121,7 @@ const GET_CART_QUERY = graphql( [MONEY_FIELDS_FRAGMENT], ); -export const getCart = cache(async (cartId?: string) => { +export const getCart = cache(async (cartId?: string, channelId?: string) => { const customerId = await getSessionCustomerId(); const response = await client.fetch({ @@ -134,6 +134,7 @@ export const getCart = cache(async (cartId?: string) => { tags: [TAGS.cart], }, }, + channelId, }); const cart = response.data.site.cart; diff --git a/core/client/queries/get-route.ts b/core/client/queries/get-route.ts index a7fc3a5564..a01f6d9d2a 100644 --- a/core/client/queries/get-route.ts +++ b/core/client/queries/get-route.ts @@ -31,11 +31,12 @@ const GET_ROUTE_QUERY = graphql(` } `); -export const getRoute = async (path: string) => { +export const getRoute = async (path: string, channelId?: string) => { const response = await client.fetch({ document: GET_ROUTE_QUERY, variables: { path }, fetchOptions: { next: { revalidate } }, + channelId, }); return response.data.site.route; diff --git a/core/client/queries/get-store-status.ts b/core/client/queries/get-store-status.ts index f32ef51de6..4498e56529 100644 --- a/core/client/queries/get-store-status.ts +++ b/core/client/queries/get-store-status.ts @@ -11,10 +11,11 @@ const GET_STORE_STATUS_QUERY = graphql(` } `); -export const getStoreStatus = async () => { +export const getStoreStatus = async (channelId?: string) => { const { data } = await client.fetch({ document: GET_STORE_STATUS_QUERY, fetchOptions: { next: { revalidate: 300 } }, + channelId, }); return data.site.settings?.status; diff --git a/core/components/header/cart-icon.tsx b/core/components/header/cart-icon.tsx index 3e6dae58f8..26c4938a1c 100644 --- a/core/components/header/cart-icon.tsx +++ b/core/components/header/cart-icon.tsx @@ -1,6 +1,7 @@ 'use client'; import { ShoppingCart } from 'lucide-react'; +import { useLocale } from 'next-intl'; import { useEffect, useState } from 'react'; import { z } from 'zod'; @@ -17,10 +18,11 @@ interface CartIconProps { export const CartIcon = ({ count }: CartIconProps) => { const [fetchedCount, setFetchedCount] = useState(); const computedCount = count ?? fetchedCount; + const locale = useLocale(); useEffect(() => { async function fetchCartQuantity() { - const response = await fetch(`/api/cart-quantity/`); + const response = await fetch(`/api/cart-quantity/?locale=${locale}`); const parsedData = CartQuantityResponseSchema.parse(await response.json()); setFetchedCount(parsedData.count); @@ -32,7 +34,7 @@ export const CartIcon = ({ count }: CartIconProps) => { if (count === undefined) { void fetchCartQuantity(); } - }, [count]); + }, [count, locale]); if (!computedCount) { return ; diff --git a/core/components/product-sheet/product-sheet-content.tsx b/core/components/product-sheet/product-sheet-content.tsx index d68e719fb1..3d24d346a3 100644 --- a/core/components/product-sheet/product-sheet-content.tsx +++ b/core/components/product-sheet/product-sheet-content.tsx @@ -2,7 +2,7 @@ import { Loader2 as Spinner } from 'lucide-react'; import { useSearchParams } from 'next/navigation'; -import { useFormatter, useTranslations } from 'next-intl'; +import { useFormatter, useLocale, useTranslations } from 'next-intl'; import { useEffect, useId, useState } from 'react'; import { FragmentOf } from '~/client/graphql'; @@ -27,6 +27,8 @@ export const ProductSheetContent = () => { const [isError, setError] = useState(false); const [product, setProduct] = useState(null); + const locale = useLocale(); + useEffect(() => { const fetchProduct = async () => { setError(false); @@ -38,6 +40,10 @@ export const ProductSheetContent = () => { } try { + const updatedSearchParams = new URLSearchParams(searchParams); + + updatedSearchParams.set('locale', locale); + const paramsString = searchParams.toString(); const queryString = `${paramsString.length ? '?' : ''}${paramsString}`; @@ -59,7 +65,7 @@ export const ProductSheetContent = () => { }; void fetchProduct(); - }, [productId, searchParams]); + }, [locale, productId, searchParams]); if (isError) { return ( diff --git a/core/middlewares/with-routes.ts b/core/middlewares/with-routes.ts index 375cebb7a5..c0a784f375 100644 --- a/core/middlewares/with-routes.ts +++ b/core/middlewares/with-routes.ts @@ -4,6 +4,7 @@ import createMiddleware from 'next-intl/middleware'; import { z } from 'zod'; import { getSessionCustomerId } from '~/auth'; +import { getChannelIdFromLocale } from '~/channels.config'; import { graphql } from '~/client/graphql'; import { getRawWebPageContent } from '~/client/queries/get-raw-web-page-content'; import { getRoute } from '~/client/queries/get-route'; @@ -65,9 +66,13 @@ const RouteCacheSchema = z.object({ expiryTime: z.number(), }); +let locale: string; + const updateRouteCache = async (pathname: string, event: NextFetchEvent): Promise => { + const channelId = getChannelIdFromLocale(locale); + const routeCache: RouteCache = { - route: await getRoute(pathname), + route: await getRoute(pathname, channelId), expiryTime: Date.now() + 1000 * 60 * 30, // 30 minutes }; @@ -77,7 +82,9 @@ const updateRouteCache = async (pathname: string, event: NextFetchEvent): Promis }; const updateStatusCache = async (event: NextFetchEvent): Promise => { - const status = await getStoreStatus(); + const channelId = getChannelIdFromLocale(locale); + + const status = await getStoreStatus(channelId); if (status === undefined) { throw new Error('Failed to fetch new storefront status'); @@ -93,7 +100,6 @@ const updateStatusCache = async (event: NextFetchEvent): Promise { let res: string; diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index d06c0efeed..ad202ce6e8 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -18,6 +18,7 @@ interface Config { platform?: string; backendUserAgentExtensions?: string; logger?: boolean; + getChannelId?: (defaultChannelId: string) => Promise | string; } interface BigCommerceResponse { @@ -26,9 +27,19 @@ interface BigCommerceResponse { class Client { private backendUserAgent: string; + private readonly defaultChannelId: string; + private getChannelId: (defaultChannelId: string) => Promise | string; constructor(private config: Config) { + if (!config.channelId) { + throw new Error('Client configuration must include a channelId.'); + } + + this.defaultChannelId = config.channelId; this.backendUserAgent = getBackendUserAgent(config.platform, config.backendUserAgentExtensions); + this.getChannelId = config.getChannelId + ? config.getChannelId + : (defaultChannelId) => defaultChannelId; } // Overload for documents that require variables @@ -66,7 +77,7 @@ class Client { const query = normalizeQuery(document); const log = this.requestLogger(query); - const graphqlUrl = this.getEndpoint(channelId); + const graphqlUrl = await this.getEndpoint(channelId); const response = await fetch(graphqlUrl, { method: 'POST', @@ -154,12 +165,10 @@ class Client { return response.json() as Promise; } - private getEndpoint(channelId?: string) { - if (!channelId && !this.config.channelId) { - throw new Error('Missing channelId'); - } + private async getEndpoint(channelId?: string) { + const resolvedChannelId = channelId ?? (await this.getChannelId(this.defaultChannelId)); - return `https://store-${this.config.storeHash}-${channelId ?? this.config.channelId}.${graphqlApiDomain}/graphql`; + return `https://store-${this.config.storeHash}-${resolvedChannelId}.${graphqlApiDomain}/graphql`; } private requestLogger(document: string) { From 1abc43630e22aa2189353fa5511399a3db00ff33 Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Tue, 2 Jul 2024 16:19:16 -0500 Subject: [PATCH 2/2] fix: remove commented line, add some comments --- .../[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx | 2 +- .../(default)/(faceted)/category/[slug]/static/page.tsx | 2 +- core/app/[locale]/(default)/product/[slug]/static/page.tsx | 2 +- core/client/index.ts | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx b/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx index 62dd98e156..06b6e3cd72 100644 --- a/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/brand/[slug]/static/page.tsx @@ -32,7 +32,7 @@ const getBrands = cache(async (variables: BrandsQueryVariables = {}) => { document: BrandsQuery, variables, fetchOptions: { next: { revalidate: revalidateTarget } }, - channelId: getChannelIdFromLocale(), + channelId: getChannelIdFromLocale(), // Using default channel id }); return removeEdgesAndNodes(response.data.site.brands); diff --git a/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx b/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx index 93c58ba1a7..ac2b4f9473 100644 --- a/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/(faceted)/category/[slug]/static/page.tsx @@ -30,7 +30,7 @@ const getCategoryTree = cache(async (variables: CategoryTreeQueryVariables = {}) document: CategoryTreeQuery, variables, fetchOptions: { next: { revalidate: revalidateTarget } }, - channelId: getChannelIdFromLocale(), + channelId: getChannelIdFromLocale(), // Using default channel id }); return response.data.site.categoryTree; diff --git a/core/app/[locale]/(default)/product/[slug]/static/page.tsx b/core/app/[locale]/(default)/product/[slug]/static/page.tsx index a9d0814f10..ea3c4db0e6 100644 --- a/core/app/[locale]/(default)/product/[slug]/static/page.tsx +++ b/core/app/[locale]/(default)/product/[slug]/static/page.tsx @@ -39,7 +39,7 @@ const getFeaturedProducts = cache(async ({ first = 12 }: Options = {}) => { variables: { first }, customerId, fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate: revalidateTarget } }, - channelId: getChannelIdFromLocale(), + channelId: getChannelIdFromLocale(), // Using default channel id }); return removeEdgesAndNodes(response.data.site.featuredProducts); diff --git a/core/client/index.ts b/core/client/index.ts index ce4d3f0b59..f3eb1dae81 100644 --- a/core/client/index.ts +++ b/core/client/index.ts @@ -1,5 +1,4 @@ import { createClient } from '@bigcommerce/catalyst-client'; -// import { headers } from 'next/headers'; import { getLocale } from 'next-intl/server'; import { getChannelIdFromLocale } from '~/channels.config';