Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Hydrogen context provider for i18n overrides #2739

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
44 changes: 44 additions & 0 deletions packages/hydrogen-react/src/HydrogenProvider.doc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs';

const data: ReferenceEntityTemplateSchema = {
name: 'HydrogenProvider',
category: 'components',
isVisualComponent: false,
related: [
{
name: 'useShop',
type: 'hook',
url: '/api/hydrogen-react/hooks/useshop',
},
],
description:
"The `HydrogenProvider` component wraps your entire Hydrogen app and provides localization data for the app. You should place it in your app's entry point component.",
type: 'component',
defaultExample: {
description: 'I am the default example',
codeblock: {
tabs: [
{
title: 'JavaScript',
code: './HydrogenProvider.example.jsx',
language: 'jsx',
},
{
title: 'TypeScript',
code: './HydrogenProvider.example.tsx',
language: 'tsx',
},
],
title: 'Example code',
},
},
definitions: [
{
title: 'Props',
type: 'HydrogenContextProps',
description: '',
},
],
};

export default data;
20 changes: 20 additions & 0 deletions packages/hydrogen-react/src/HydrogenProvider.example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {HydrogenProvider, useShop} from '@shopify/hydrogen-react';

export default function App() {
return (
<HydrogenProvider countryIsoCode="CA" languageIsoCode="EN">
<UsingUseShop />
</HydrogenProvider>
);
}

export function UsingUseShop() {
const shop = useShop();

return (
<>
<div>{shop.languageIsoCode}</div>
<div>{shop.countryIsoCode}</div>
</>
);
}
20 changes: 20 additions & 0 deletions packages/hydrogen-react/src/HydrogenProvider.example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {HydrogenProvider, useShop} from '@shopify/hydrogen-react';

export default function App() {
return (
<HydrogenProvider countryIsoCode="CA" languageIsoCode="EN">
<UsingUseShop />
</HydrogenProvider>
);
}

export function UsingUseShop() {
const shop = useShop();

return (
<>
<div>{shop.languageIsoCode}</div>
<div>{shop.countryIsoCode}</div>
</>
);
}
30 changes: 30 additions & 0 deletions packages/hydrogen-react/src/HydrogenProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {describe, expect, it} from 'vitest';

import {render, screen, renderHook} from '@testing-library/react';
import {useShop} from './ShopifyProvider.js';
import {HydrogenProvider} from './HydrogenProvider.js';

describe('<HydrogenProvider/>', () => {
it('renders its children', () => {
render(
<HydrogenProvider countryIsoCode={'US'} languageIsoCode={'EN'}>
<div>child</div>;
</HydrogenProvider>,
);

expect(screen.getByText('child')).toBeInTheDocument();
});

it('returns the hydrogen context values', () => {
const {result} = renderHook(() => useShop(), {
wrapper: ({children}) => (
<HydrogenProvider countryIsoCode={'CA'} languageIsoCode={'FR'}>
{children}
</HydrogenProvider>
),
});

expect(result.current.countryIsoCode).toBe('CA');
expect(result.current.languageIsoCode).toBe('FR');
});
});
44 changes: 44 additions & 0 deletions packages/hydrogen-react/src/HydrogenProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {createContext, ReactNode, useContext} from 'react';
import {CountryCode, LanguageCode} from './storefront-api-types.js';

export type HydrogenContextValue = {
/**
* The code designating a country, which generally follows ISO 3166-1 alpha-2 guidelines. If a territory doesn't have a country code value in the `CountryCode` enum, it might be considered a subdivision of another country. For example, the territories associated with Spain are represented by the country code `ES`, and the territories associated with the United States of America are represented by the country code `US`.
*/
countryIsoCode: CountryCode | null;
/**
* `ISO 369` language codes supported by Shopify.
*/
languageIsoCode: LanguageCode | null;
};

const defaultHydrogenContext: HydrogenContextValue = {
languageIsoCode: null,
countryIsoCode: null,
};

const HydrogenContext = createContext<HydrogenContextValue>(
defaultHydrogenContext,
);

export interface HydrogenProviderProps extends HydrogenContextValue {
children: ReactNode;
}

/**
* The `<HydrogenProvider/>` component enables use of the `useShop()` hook. The component should wrap your Hydrogen app.
*/
export function HydrogenProvider({
children,
...hydrogenConfig
}: HydrogenProviderProps): JSX.Element {
return (
<HydrogenContext.Provider value={hydrogenConfig}>
{children}
</HydrogenContext.Provider>
);
}

export function useHydrogenContext(): HydrogenContextValue {
return useContext(HydrogenContext);
}
45 changes: 45 additions & 0 deletions packages/hydrogen-react/src/ShopifyProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type ShopifyProviderProps,
} from './ShopifyProvider.js';
import type {PartialDeep} from 'type-fest';
import {HydrogenProvider} from './HydrogenProvider.js';

const SHOPIFY_CONFIG: ShopifyProviderProps = {
storeDomain: 'https://notashop.myshopify.com',
Expand Down Expand Up @@ -223,6 +224,50 @@ describe('<ShopifyProvider/>', () => {
);
});
});

describe('hydrogen context overrides', () => {
it('returns the hydrogen overrides if provided (partial override)', () => {
const {result} = renderHook(() => useShop(), {
wrapper: ({children}) => (
<HydrogenProvider countryIsoCode={'FR'} languageIsoCode={null}>
<ShopifyProvider {...SHOPIFY_CONFIG}>{children}</ShopifyProvider>
</HydrogenProvider>
),
});

expect(result.current.countryIsoCode).toBe('FR');
expect(result.current.languageIsoCode).toBe(
SHOPIFY_CONFIG.languageIsoCode,
);
});

it('returns the hydrogen overrides if provided (full override)', () => {
const {result} = renderHook(() => useShop(), {
wrapper: ({children}) => (
<HydrogenProvider countryIsoCode={'FR'} languageIsoCode={'FR'}>
<ShopifyProvider {...SHOPIFY_CONFIG}>{children}</ShopifyProvider>
</HydrogenProvider>
),
});
expect(result.current.countryIsoCode).toBe('FR');
expect(result.current.languageIsoCode).toBe('FR');
});

it('returns the hydrogen overrides if provided (full override, without ShopifyProvider)', () => {
// Note(FR): this ensures the current behavior – however it's arguable that not having the ShopifyProvider at all
// should not be possible, as docs currently say it _must_ be there.
const {result} = renderHook(() => useShop(), {
wrapper: ({children}) => (
<HydrogenProvider countryIsoCode={'FR'} languageIsoCode={'FR'}>
{children}
</HydrogenProvider>
),
});

expect(result.current.countryIsoCode).toBe('FR');
expect(result.current.languageIsoCode).toBe('FR');
});
});
});

export function getShopifyConfig(
Expand Down
12 changes: 11 additions & 1 deletion packages/hydrogen-react/src/ShopifyProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {createContext, useContext, useMemo, type ReactNode} from 'react';
import type {LanguageCode, CountryCode} from './storefront-api-types.js';
import {SFAPI_VERSION} from './storefront-api-constants.js';
import {getPublicTokenHeadersRaw} from './storefront-client.js';
import {useHydrogenContext} from './HydrogenProvider.js';

export const defaultShopifyContext: ShopifyContextValue = {
storeDomain: 'test',
Expand Down Expand Up @@ -94,7 +95,16 @@ export function useShop(): ShopifyContextValue {
if (!shopContext) {
throw new Error(`'useShop()' must be a descendent of <ShopifyProvider/>`);
}
return shopContext;

const hydrogenContext = useHydrogenContext();

return {
...shopContext,
countryIsoCode:
hydrogenContext.countryIsoCode ?? shopContext.countryIsoCode,
languageIsoCode:
hydrogenContext.languageIsoCode ?? shopContext.languageIsoCode,
};
}

export interface ShopifyProviderBase {
Expand Down
1 change: 1 addition & 0 deletions packages/hydrogen-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export {
type MappedProductOptions,
mapSelectedProductOptionToObject,
} from './getProductOptions.js';
export {HydrogenProvider} from './HydrogenProvider.js';
export {Image, IMAGE_FRAGMENT} from './Image.js';
export {useLoadScript} from './load-script.js';
export {MediaFile} from './MediaFile.js';
Expand Down
34 changes: 21 additions & 13 deletions templates/skeleton/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useNonce, getShopAnalytics, Analytics} from '@shopify/hydrogen';
import {HydrogenProvider} from '@shopify/hydrogen-react';
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
import {
Links,
Expand Down Expand Up @@ -69,6 +70,7 @@ export async function loader(args: LoaderFunctionArgs) {
const criticalData = await loadCriticalData(args);

const {storefront, env} = args.context;
const {i18n} = storefront;

return defer({
...deferredData,
Expand All @@ -83,9 +85,10 @@ export async function loader(args: LoaderFunctionArgs) {
storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
withPrivacyBanner: false,
// localize the privacy banner
country: args.context.storefront.i18n.country,
language: args.context.storefront.i18n.language,
country: i18n.country,
language: i18n.language,
},
i18n,
});
}

Expand Down Expand Up @@ -152,17 +155,22 @@ export function Layout({children}: {children?: React.ReactNode}) {
<Links />
</head>
<body>
{data ? (
<Analytics.Provider
cart={data.cart}
shop={data.shop}
consent={data.consent}
>
<PageLayout {...data}>{children}</PageLayout>
</Analytics.Provider>
) : (
children
)}
<HydrogenProvider
countryIsoCode={data?.i18n.country ?? null}
languageIsoCode={data?.i18n.language ?? null}
>
{data ? (
<Analytics.Provider
cart={data.cart}
shop={data.shop}
consent={data.consent}
>
<PageLayout {...data}>{children}</PageLayout>
</Analytics.Provider>
) : (
children
)}
</HydrogenProvider>
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
</body>
Expand Down