diff --git a/.changeset/hydrogen-shopify-provider-reexport.md b/.changeset/hydrogen-shopify-provider-reexport.md
new file mode 100644
index 0000000000..e73a165536
--- /dev/null
+++ b/.changeset/hydrogen-shopify-provider-reexport.md
@@ -0,0 +1,5 @@
+---
+"@shopify/hydrogen": patch
+---
+
+Re-export `ShopifyProvider`, `useShop`, and `SFAPI_VERSION` from `@shopify/hydrogen` so these can be imported without reaching into `@shopify/hydrogen-react` directly. Add `apiVersion` to the `storefront` object returned by `createStorefrontClient`, giving loaders access to the resolved Storefront API version.
diff --git a/.changeset/skeleton-shopify-provider.md b/.changeset/skeleton-shopify-provider.md
new file mode 100644
index 0000000000..2df5dc7344
--- /dev/null
+++ b/.changeset/skeleton-shopify-provider.md
@@ -0,0 +1,7 @@
+---
+"skeleton": patch
+"@shopify/cli-hydrogen": patch
+"@shopify/create-hydrogen": patch
+---
+
+Add `ShopifyProvider` to the skeleton template so that `Money` and other locale-aware components format values using your store's configured locale instead of defaulting to `en-US`. Also sets the `` attribute dynamically based on the store's language setting.
diff --git a/cookbook/recipes/b2b/patches/root.tsx.8c60e9.patch b/cookbook/recipes/b2b/patches/root.tsx.8c60e9.patch
index c0d86c4c17..75750180d5 100644
--- a/cookbook/recipes/b2b/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/b2b/patches/root.tsx.8c60e9.patch
@@ -1,7 +1,7 @@
-index df87425c..5a0fef09 100644
+index b7eb81513..c2ae52c9b 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -16,9 +16,39 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
+@@ -21,9 +21,39 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
import resetStyles from '~/styles/reset.css?url';
import appStyles from '~/styles/app.css?url';
import {PageLayout} from './components/PageLayout';
@@ -41,20 +41,20 @@ index df87425c..5a0fef09 100644
/**
* This is important to avoid re-fetching root queries on sub-navigations
*/
-@@ -176,9 +206,13 @@ export default function App() {
- shop={data.shop}
- consent={data.consent}
- >
--
--
--
-+ {/* @description Wrap PageLayout with B2B location provider for company location management */}
-+
-+
-+
-+
-+
-+
-
+@@ -193,9 +223,13 @@ export default function App() {
+ shop={data.shop}
+ consent={data.consent}
+ >
+-
+-
+-
++ {/* @description Wrap PageLayout with B2B location provider for company location management */}
++
++
++
++
++
++
+
+
);
- }
diff --git a/cookbook/recipes/express/patches/root.tsx.8c60e9.patch b/cookbook/recipes/express/patches/root.tsx.8c60e9.patch
index ebd7551343..22d9e7a8d4 100644
--- a/cookbook/recipes/express/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/express/patches/root.tsx.8c60e9.patch
@@ -1,7 +1,7 @@
-index df87425c..1ba9888f 100644
+index b7eb81513..859bccb7e 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -11,46 +11,20 @@ import {
+@@ -16,46 +16,20 @@ import {
useRouteLoaderData,
} from 'react-router';
import type {Route} from './+types/root';
@@ -49,7 +49,7 @@ index df87425c..1ba9888f 100644
export function links() {
return [
{
-@@ -61,18 +35,19 @@ export function links() {
+@@ -66,18 +40,19 @@ export function links() {
rel: 'preconnect',
href: 'https://shop.app',
},
@@ -76,7 +76,7 @@ index df87425c..1ba9888f 100644
return {
...deferredData,
-@@ -86,59 +61,29 @@ export async function loader(args: Route.LoaderArgs) {
+@@ -94,59 +69,29 @@ export async function loader(args: Route.LoaderArgs) {
checkoutDomain: env.PUBLIC_CHECKOUT_DOMAIN,
storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
withPrivacyBanner: false,
@@ -149,7 +149,7 @@ index df87425c..1ba9888f 100644
}
export function Layout({children}: {children?: React.ReactNode}) {
-@@ -149,8 +94,7 @@ export function Layout({children}: {children?: React.ReactNode}) {
+@@ -159,8 +104,7 @@ export function Layout({children}: {children?: React.ReactNode}) {
@@ -159,21 +159,21 @@ index df87425c..1ba9888f 100644
-@@ -176,9 +120,11 @@ export default function App() {
- shop={data.shop}
- consent={data.consent}
- >
--
-+
-+
{data.layout?.shop?.name} (Express example)
-+ {data.layout?.shop?.description}
-
--
-+
-
+@@ -193,9 +137,11 @@ export default function App() {
+ shop={data.shop}
+ consent={data.consent}
+ >
+-
++
++
{data.layout?.shop?.name} (Express example)
++ {data.layout?.shop?.description}
+
+-
++
+
+
);
- }
-@@ -207,3 +153,19 @@ export function ErrorBoundary() {
+@@ -225,3 +171,19 @@ export function ErrorBoundary() {
);
}
diff --git a/cookbook/recipes/gtm/patches/root.tsx.5e9998.patch b/cookbook/recipes/gtm/patches/root.tsx.5e9998.patch
index 013e333a98..8be3948e6c 100644
--- a/cookbook/recipes/gtm/patches/root.tsx.5e9998.patch
+++ b/cookbook/recipes/gtm/patches/root.tsx.5e9998.patch
@@ -1,13 +1,15 @@
-index df87425c..aa25c6d7 100644
+index b7eb81513..65cf4e992 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -1,4 +1,4 @@
--import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
-+import {Analytics, getShopAnalytics, useNonce, Script} from '@shopify/hydrogen';
+@@ -3,6 +3,7 @@ import {
+ getShopAnalytics,
+ ShopifyProvider,
+ useNonce,
++ Script,
+ } from '@shopify/hydrogen';
import {
Outlet,
- useRouteError,
-@@ -16,6 +16,7 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
+@@ -21,6 +22,7 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
import resetStyles from '~/styles/reset.css?url';
import appStyles from '~/styles/app.css?url';
import {PageLayout} from './components/PageLayout';
@@ -15,7 +17,7 @@ index df87425c..aa25c6d7 100644
export type RootLoader = typeof loader;
-@@ -153,8 +154,32 @@ export function Layout({children}: {children?: React.ReactNode}) {
+@@ -163,8 +165,32 @@ export function Layout({children}: {children?: React.ReactNode}) {
@@ -48,12 +50,12 @@ index df87425c..aa25c6d7 100644
{children}
-@@ -179,6 +204,8 @@ export default function App() {
-
-
-
-+ {/* @description Initialize Google Tag Manager analytics integration */}
-+
-
+@@ -196,6 +222,8 @@ export default function App() {
+
+
+
++ {/* @description Initialize Google Tag Manager analytics integration */}
++
+
+
);
- }
diff --git a/cookbook/recipes/legacy-customer-account-flow/patches/root.tsx.8c60e9.patch b/cookbook/recipes/legacy-customer-account-flow/patches/root.tsx.8c60e9.patch
index e4c68a2cdf..f5ebef4785 100644
--- a/cookbook/recipes/legacy-customer-account-flow/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/legacy-customer-account-flow/patches/root.tsx.8c60e9.patch
@@ -1,14 +1,15 @@
-index df87425c..aa6f5166 100644
+index b7eb81513..7205a0143 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -1,5 +1,6 @@
- import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
+@@ -5,6 +5,7 @@ import {
+ useNonce,
+ } from '@shopify/hydrogen';
import {
+ data,
Outlet,
useRouteError,
isRouteErrorResponse,
-@@ -11,6 +12,7 @@ import {
+@@ -16,6 +17,7 @@ import {
useRouteLoaderData,
} from 'react-router';
import type {Route} from './+types/root';
@@ -16,7 +17,7 @@ index df87425c..aa6f5166 100644
import favicon from '~/assets/favicon.svg';
import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
import resetStyles from '~/styles/reset.css?url';
-@@ -65,6 +67,9 @@ export function links() {
+@@ -70,6 +72,9 @@ export function links() {
];
}
@@ -26,7 +27,7 @@ index df87425c..aa6f5166 100644
export async function loader(args: Route.LoaderArgs) {
// Start fetching non-critical data without blocking time to first byte
const deferredData = loadDeferredData(args);
-@@ -74,23 +79,38 @@ export async function loader(args: Route.LoaderArgs) {
+@@ -79,26 +84,41 @@ export async function loader(args: Route.LoaderArgs) {
const {storefront, env} = args.context;
@@ -34,6 +35,9 @@ index df87425c..aa6f5166 100644
- ...deferredData,
- ...criticalData,
- publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
+- publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
+- storefrontApiVersion: storefront.apiVersion,
+- i18n: storefront.i18n,
- shop: getShopAnalytics({
- storefront,
- publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
@@ -61,6 +65,9 @@ index df87425c..aa6f5166 100644
+ // @description Include isLoggedIn status for legacy authentication
+ isLoggedIn,
+ publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
++ publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
++ storefrontApiVersion: storefront.apiVersion,
++ i18n: storefront.i18n,
+ shop: getShopAnalytics({
+ storefront,
+ publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
@@ -81,7 +88,7 @@ index df87425c..aa6f5166 100644
}
/**
-@@ -207,3 +227,39 @@ export function ErrorBoundary() {
+@@ -225,3 +245,39 @@ export function ErrorBoundary() {
);
}
diff --git a/cookbook/recipes/markets/patches/root.tsx.9594cb.patch b/cookbook/recipes/markets/patches/root.tsx.9594cb.patch
index 7cd7407428..d2fa3d7303 100644
--- a/cookbook/recipes/markets/patches/root.tsx.9594cb.patch
+++ b/cookbook/recipes/markets/patches/root.tsx.9594cb.patch
@@ -1,23 +1,23 @@
-index df87425c..97ca8174 100644
+index b7eb81513..7a64b33e8 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -77,6 +77,7 @@ export async function loader(args: Route.LoaderArgs) {
+@@ -82,6 +82,7 @@ export async function loader(args: Route.LoaderArgs) {
return {
...deferredData,
...criticalData,
+ selectedLocale: args.context.storefront.i18n,
publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
- shop: getShopAnalytics({
- storefront,
-@@ -176,7 +177,10 @@ export default function App() {
- shop={data.shop}
- consent={data.consent}
- >
--
-+
-
-
-
+ publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
+ storefrontApiVersion: storefront.apiVersion,
+@@ -193,7 +194,10 @@ export default function App() {
+ shop={data.shop}
+ consent={data.consent}
+ >
+-
++
+
+
+
diff --git a/cookbook/recipes/metaobjects/patches/root.tsx.8c60e9.patch b/cookbook/recipes/metaobjects/patches/root.tsx.8c60e9.patch
index fbe9fb46ec..953d7ebcff 100644
--- a/cookbook/recipes/metaobjects/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/metaobjects/patches/root.tsx.8c60e9.patch
@@ -1,7 +1,7 @@
-index df87425c..12434050 100644
+index b7eb81513..c265a7e69 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -90,6 +90,8 @@ export async function loader(args: Route.LoaderArgs) {
+@@ -98,6 +98,8 @@ export async function loader(args: Route.LoaderArgs) {
country: args.context.storefront.i18n.country,
language: args.context.storefront.i18n.language,
},
diff --git a/cookbook/recipes/multipass/patches/root.tsx.8c60e9.patch b/cookbook/recipes/multipass/patches/root.tsx.8c60e9.patch
index cf5b5ae939..4aeadf270c 100644
--- a/cookbook/recipes/multipass/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/multipass/patches/root.tsx.8c60e9.patch
@@ -1,14 +1,12 @@
-index df87425c..a2c2acc8 100644
+index b7eb81513..aeefab1a5 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -1,4 +1,15 @@
--import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
-+import {
-+ Analytics,
-+ getShopAnalytics,
-+ useNonce,
+@@ -3,7 +3,14 @@ import {
+ getShopAnalytics,
+ ShopifyProvider,
+ useNonce,
+ type HydrogenSession,
-+} from '@shopify/hydrogen';
+ } from '@shopify/hydrogen';
+
+// @description Define CustomerAccessToken type for multipass
+type CustomerAccessToken = {
@@ -18,7 +16,7 @@ index df87425c..a2c2acc8 100644
import {
Outlet,
useRouteError,
-@@ -110,7 +121,14 @@ async function loadCriticalData({context}: Route.LoaderArgs) {
+@@ -118,7 +125,14 @@ async function loadCriticalData({context}: Route.LoaderArgs) {
// Add other queries here, so that they are loaded in parallel
]);
@@ -34,7 +32,7 @@ index df87425c..a2c2acc8 100644
}
/**
-@@ -207,3 +225,24 @@ export function ErrorBoundary() {
+@@ -225,3 +239,24 @@ export function ErrorBoundary() {
);
}
diff --git a/cookbook/recipes/partytown/patches/root.tsx.8c60e9.patch b/cookbook/recipes/partytown/patches/root.tsx.8c60e9.patch
index 3083d48225..361ace16aa 100644
--- a/cookbook/recipes/partytown/patches/root.tsx.8c60e9.patch
+++ b/cookbook/recipes/partytown/patches/root.tsx.8c60e9.patch
@@ -1,13 +1,15 @@
-index df87425c..a2b8986a 100644
+index b7eb81513..401f31e87 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
-@@ -1,4 +1,4 @@
--import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
-+import {Analytics, getShopAnalytics, useNonce, Script} from '@shopify/hydrogen';
+@@ -3,6 +3,7 @@ import {
+ getShopAnalytics,
+ ShopifyProvider,
+ useNonce,
++ Script,
+ } from '@shopify/hydrogen';
import {
Outlet,
- useRouteError,
-@@ -16,6 +16,10 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
+@@ -21,6 +22,10 @@ import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments';
import resetStyles from '~/styles/reset.css?url';
import appStyles from '~/styles/app.css?url';
import {PageLayout} from './components/PageLayout';
@@ -18,7 +20,7 @@ index df87425c..a2b8986a 100644
export type RootLoader = typeof loader;
-@@ -90,6 +94,10 @@ export async function loader(args: Route.LoaderArgs) {
+@@ -98,6 +103,10 @@ export async function loader(args: Route.LoaderArgs) {
country: args.context.storefront.i18n.country,
language: args.context.storefront.i18n.language,
},
@@ -29,7 +31,7 @@ index df87425c..a2b8986a 100644
};
}
-@@ -163,6 +171,38 @@ export function Layout({children}: {children?: React.ReactNode}) {
+@@ -173,6 +182,38 @@ export function Layout({children}: {children?: React.ReactNode}) {
);
}
@@ -68,11 +70,11 @@ index df87425c..a2b8986a 100644
export default function App() {
const data = useRouteLoaderData('root');
-@@ -177,6 +217,7 @@ export default function App() {
- consent={data.consent}
- >
-
-+
-
-
-
+@@ -194,6 +235,7 @@ export default function App() {
+ consent={data.consent}
+ >
+
++
+
+
+
diff --git a/e2e/fixtures/currency-formats.ts b/e2e/fixtures/currency-formats.ts
index ba3f6428ae..9510dc4a11 100644
--- a/e2e/fixtures/currency-formats.ts
+++ b/e2e/fixtures/currency-formats.ts
@@ -7,5 +7,8 @@
*/
export const CURRENCY_FORMATS = {
USD: /^\$[\d,]+\.\d{2}$/,
- CAD: /^CA\$[\d,]+\.\d{2}$/,
+ /** English-formatted CAD price, e.g. CA$1,121.00 */
+ CAD_EN: /^CA\$[\d,]+\.\d{2}$/,
+ /** French-Canadian-formatted CAD price, e.g. 1 121,00 $ */
+ CAD_FR: /^[\d\u00A0\u202F ]+,\d{2}\s*\$$/,
} as const;
diff --git a/e2e/specs/recipes/markets.spec.ts b/e2e/specs/recipes/markets.spec.ts
index 4675020ed9..a8e31cfff5 100644
--- a/e2e/specs/recipes/markets.spec.ts
+++ b/e2e/specs/recipes/markets.spec.ts
@@ -48,7 +48,7 @@ test.describe('Markets Recipe', () => {
await page.goto(`/FR-CA/products/${KNOWN_PRODUCT.handle}`);
const priceElement = recipe.getPriceElement();
- await recipe.assertPriceFormat(priceElement, CURRENCY_FORMATS.CAD);
+ await recipe.assertPriceFormat(priceElement, CURRENCY_FORMATS.CAD_FR);
});
test('collection page URL includes locale prefix and products link with locale', async ({
@@ -75,12 +75,12 @@ test.describe('Markets Recipe', () => {
// CAD in the drawer proves AddToCartButton posted to /FR-CA/cart rather than /cart,
// creating the cart with the correct market context.
- await recipe.assertCartSubtotalFormat(CURRENCY_FORMATS.CAD);
+ await recipe.assertCartSubtotalFormat(CURRENCY_FORMATS.CAD_FR);
await page.goto('/FR-CA/cart');
await page.waitForURL(/\/FR-CA\/cart$/);
- await recipe.assertCartSubtotalFormat(CURRENCY_FORMATS.CAD);
+ await recipe.assertCartSubtotalFormat(CURRENCY_FORMATS.CAD_FR);
});
});
diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts
index 5dadfadecb..4fbab96cdd 100644
--- a/packages/hydrogen/src/index.ts
+++ b/packages/hydrogen/src/index.ts
@@ -168,11 +168,14 @@ export {
parseGid,
parseMetafield,
sendShopifyAnalytics,
+ ShopifyProvider,
ShopifySalesChannel,
storefrontApiCustomScalars,
useLoadScript,
useMoney,
useSelectedOptionInUrlParam,
+ useShop,
useShopifyCookies,
Video,
} from '@shopify/hydrogen-react';
+export {SFAPI_VERSION} from '@shopify/hydrogen-react/storefront-api-constants';
diff --git a/packages/hydrogen/src/storefront.ts b/packages/hydrogen/src/storefront.ts
index 4fd2e520ba..152defff2d 100644
--- a/packages/hydrogen/src/storefront.ts
+++ b/packages/hydrogen/src/storefront.ts
@@ -7,6 +7,7 @@ import {
SHOPIFY_VISIT_TOKEN_HEADER,
type StorefrontClientProps,
} from '@shopify/hydrogen-react';
+import {SFAPI_VERSION} from '@shopify/hydrogen-react/storefront-api-constants';
import type {WritableDeep} from 'type-fest';
import {fetchWithServerCache} from './cache/server-fetch';
import {
@@ -173,6 +174,8 @@ export type Storefront = {
typeof createStorefrontUtilities
>['getStorefrontApiUrl'];
i18n: TI18n;
+ /** The resolved Storefront API version used by this client. */
+ readonly apiVersion: string;
getHeaders: () => Record;
/**
* Checks if the request URL matches the Storefront API GraphQL endpoint.
@@ -555,6 +558,7 @@ export function createStorefrontClient(
getShopifyDomain,
getApiUrl: getStorefrontApiUrl,
i18n: (i18n ?? defaultI18n) as TI18n,
+ apiVersion: clientOptions.storefrontApiVersion ?? SFAPI_VERSION,
/**
* Checks if the request is targeting the Storefront API endpoint.
@@ -825,6 +829,8 @@ export type StorefrontForDoc = {
>['getStorefrontApiUrl'];
/** The `i18n` object passed in from the `createStorefrontClient` argument. */
i18n?: TI18n;
+ /** The resolved Storefront API version used by this client. */
+ readonly apiVersion?: string;
};
export type StorefrontQueryOptionsForDocs = {
diff --git a/templates/skeleton/app/root.tsx b/templates/skeleton/app/root.tsx
index df87425c57..c502c508a6 100644
--- a/templates/skeleton/app/root.tsx
+++ b/templates/skeleton/app/root.tsx
@@ -1,4 +1,9 @@
-import {Analytics, getShopAnalytics, useNonce} from '@shopify/hydrogen';
+import {
+ Analytics,
+ getShopAnalytics,
+ ShopifyProvider,
+ useNonce,
+} from '@shopify/hydrogen';
import {
Outlet,
useRouteError,
@@ -78,6 +83,9 @@ export async function loader(args: Route.LoaderArgs) {
...deferredData,
...criticalData,
publicStoreDomain: env.PUBLIC_STORE_DOMAIN,
+ publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
+ storefrontApiVersion: storefront.apiVersion,
+ i18n: storefront.i18n,
shop: getShopAnalytics({
storefront,
publicStorefrontId: env.PUBLIC_STOREFRONT_ID,
@@ -143,9 +151,11 @@ function loadDeferredData({context}: Route.LoaderArgs) {
export function Layout({children}: {children?: React.ReactNode}) {
const nonce = useNonce();
+ const data = useRouteLoaderData('root');
+ const locale = data?.i18n?.language?.toLowerCase() ?? 'en';
return (
-
+
@@ -171,15 +181,23 @@ export default function App() {
}
return (
-
-
-
-
-
+
+
+
+
+
+
);
}