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

Make client.fetch channel aware per locale.
5 changes: 5 additions & 0 deletions .changeset/smooth-trains-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-client": patch
---

Add getChannelId param to dynamically fetch a channel on requests.
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -31,6 +32,7 @@ const getBrands = cache(async (variables: BrandsQueryVariables = {}) => {
document: BrandsQuery,
variables,
fetchOptions: { next: { revalidate: revalidateTarget } },
channelId: getChannelIdFromLocale(), // Using default channel id
});

return removeEdgesAndNodes(response.data.site.brands);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -29,6 +30,7 @@ const getCategoryTree = cache(async (variables: CategoryTreeQueryVariables = {})
document: CategoryTreeQuery,
variables,
fetchOptions: { next: { revalidate: revalidateTarget } },
channelId: getChannelIdFromLocale(), // Using default channel id
});

return response.data.site.categoryTree;
Expand Down
4 changes: 2 additions & 2 deletions core/app/[locale]/(default)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ const LayoutQuery = graphql(
);

export default async function DefaultLayout({ children, params: { locale } }: Props) {
unstable_setRequestLocale(locale);

const customerId = await getSessionCustomerId();

const { data } = await client.fetch({
document: LayoutQuery,
fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } },
});

unstable_setRequestLocale(locale);

const messages = await getMessages({ locale });

return (
Expand Down
4 changes: 2 additions & 2 deletions core/app/[locale]/(default)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
2 changes: 2 additions & 0 deletions core/app/[locale]/(default)/product/[slug]/static/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,6 +39,7 @@ const getFeaturedProducts = cache(async ({ first = 12 }: Options = {}) => {
variables: { first },
customerId,
fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate: revalidateTarget } },
channelId: getChannelIdFromLocale(), // Using default channel id
});

return removeEdgesAndNodes(response.data.site.featuredProducts);
Expand Down
10 changes: 7 additions & 3 deletions core/app/api/cart-quantity/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Expand Down
6 changes: 6 additions & 0 deletions core/app/api/product/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand All @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions core/channels.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { type LocaleType } from './i18n';

export type RecordFromLocales = {
[K in LocaleType]: string;
};

// Set overrides per locale
const localeToChannelsMappings: Partial<RecordFromLocales> = {
// 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;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning process.env.BIGCOMMERCE_CHANNEL_ID until we can figure out how to programatically detect if getLocale won't work in getChannelId.

}

export { getChannelIdFromLocale };
26 changes: 26 additions & 0 deletions core/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createClient } from '@bigcommerce/catalyst-client';
import { getLocale } from 'next-intl/server';

import { getChannelIdFromLocale } from '~/channels.config';

import { backendUserAgent } from '../userAgent';

Expand All @@ -11,4 +14,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;
}
},
});
3 changes: 2 additions & 1 deletion core/client/queries/get-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -134,6 +134,7 @@ export const getCart = cache(async (cartId?: string) => {
tags: [TAGS.cart],
},
},
channelId,
});

const cart = response.data.site.cart;
Expand Down
3 changes: 2 additions & 1 deletion core/client/queries/get-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion core/client/queries/get-store-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions core/components/header/cart-icon.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -17,10 +18,11 @@ interface CartIconProps {
export const CartIcon = ({ count }: CartIconProps) => {
const [fetchedCount, setFetchedCount] = useState<number | null>();
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);
Expand All @@ -32,7 +34,7 @@ export const CartIcon = ({ count }: CartIconProps) => {
if (count === undefined) {
void fetchCartQuantity();
}
}, [count]);
}, [count, locale]);

if (!computedCount) {
return <ShoppingCart aria-label="cart" />;
Expand Down
10 changes: 8 additions & 2 deletions core/components/product-sheet/product-sheet-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,6 +27,8 @@ export const ProductSheetContent = () => {
const [isError, setError] = useState(false);
const [product, setProduct] = useState<Product | null>(null);

const locale = useLocale();

useEffect(() => {
const fetchProduct = async () => {
setError(false);
Expand All @@ -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}`;

Expand All @@ -59,7 +65,7 @@ export const ProductSheetContent = () => {
};

void fetchProduct();
}, [productId, searchParams]);
}, [locale, productId, searchParams]);

if (isError) {
return (
Expand Down
12 changes: 9 additions & 3 deletions core/middlewares/with-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,9 +66,13 @@ const RouteCacheSchema = z.object({
expiryTime: z.number(),
});

let locale: string;

const updateRouteCache = async (pathname: string, event: NextFetchEvent): Promise<RouteCache> => {
const channelId = getChannelIdFromLocale(locale);

const routeCache: RouteCache = {
route: await getRoute(pathname),
route: await getRoute(pathname, channelId),
expiryTime: Date.now() + 1000 * 60 * 30, // 30 minutes
};

Expand All @@ -77,7 +82,9 @@ const updateRouteCache = async (pathname: string, event: NextFetchEvent): Promis
};

const updateStatusCache = async (event: NextFetchEvent): Promise<StorefrontStatusCache> => {
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');
Expand All @@ -93,7 +100,6 @@ const updateStatusCache = async (event: NextFetchEvent): Promise<StorefrontStatu
return statusCache;
};

let locale: string;
const clearLocaleFromPath = (path: string) => {
let res: string;

Expand Down
21 changes: 15 additions & 6 deletions packages/client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface Config {
platform?: string;
backendUserAgentExtensions?: string;
logger?: boolean;
getChannelId?: (defaultChannelId: string) => Promise<string> | string;
}

interface BigCommerceResponse<T> {
Expand All @@ -26,9 +27,19 @@ interface BigCommerceResponse<T> {

class Client<FetcherRequestInit extends RequestInit = RequestInit> {
private backendUserAgent: string;
private readonly defaultChannelId: string;
private getChannelId: (defaultChannelId: string) => Promise<string> | 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
Expand Down Expand Up @@ -66,7 +77,7 @@ class Client<FetcherRequestInit extends RequestInit = RequestInit> {
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',
Expand Down Expand Up @@ -154,12 +165,10 @@ class Client<FetcherRequestInit extends RequestInit = RequestInit> {
return response.json() as Promise<unknown>;
}

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) {
Expand Down