@cartbase/storefront 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +4 -1
  3. package/src/api/auth.ts +108 -108
  4. package/src/api/carts.ts +523 -523
  5. package/src/api/categories.ts +184 -184
  6. package/src/api/checkout.ts +526 -526
  7. package/src/api/collections.ts +130 -130
  8. package/src/api/consent.ts +75 -75
  9. package/src/api/content.ts +125 -125
  10. package/src/api/customers.ts +303 -303
  11. package/src/api/gift-cards.ts +112 -112
  12. package/src/api/http.ts +181 -180
  13. package/src/api/index.ts +30 -30
  14. package/src/api/menus.ts +77 -77
  15. package/src/api/metaobjects.ts +136 -136
  16. package/src/api/orders.ts +290 -290
  17. package/src/api/redirects.ts +37 -37
  18. package/src/api/regions.ts +200 -200
  19. package/src/api/search.ts +163 -163
  20. package/src/api/store.ts +56 -35
  21. package/src/api/types.ts +91 -91
  22. package/src/cart-drawer/context.tsx +778 -778
  23. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  24. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  25. package/src/cart-drawer/gift-wrap.tsx +82 -82
  26. package/src/cart-drawer/item/index.tsx +162 -162
  27. package/src/cart-drawer/item/upsell.tsx +110 -110
  28. package/src/cart-drawer/labels.ts +123 -123
  29. package/src/cart-drawer/mutation-queue.ts +78 -78
  30. package/src/cart-drawer/notes.tsx +131 -131
  31. package/src/cart-drawer/payment-badges.tsx +36 -96
  32. package/src/cart-drawer/sticky-footer.tsx +73 -73
  33. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  34. package/src/checkout/address-error-copy.ts +117 -117
  35. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  36. package/src/checkout/compare-addresses.ts +40 -40
  37. package/src/checkout/discount-section.tsx +218 -218
  38. package/src/checkout/error-copy-codes.ts +63 -63
  39. package/src/checkout/geocode.ts +154 -154
  40. package/src/checkout/gift-card-section.tsx +224 -224
  41. package/src/checkout/index.ts +72 -72
  42. package/src/checkout/labels.ts +495 -495
  43. package/src/checkout/payment-button.tsx +372 -372
  44. package/src/checkout/payment-error-copy.ts +154 -154
  45. package/src/checkout/promotion-error-copy.ts +91 -91
  46. package/src/common/country-flag.tsx +52 -52
  47. package/src/common/country-select.tsx +11 -11
  48. package/src/common/icons/cartbase-mark.ts +9 -0
  49. package/src/common/icons/payment-marks.ts +73 -0
  50. package/src/common/icons/social-marks.ts +61 -0
  51. package/src/common/index.ts +35 -20
  52. package/src/common/market-select.tsx +57 -57
  53. package/src/common/payment-icons.tsx +54 -0
  54. package/src/common/powered-by-cartbase.tsx +47 -0
  55. package/src/common/social-links.tsx +65 -0
  56. package/src/index.ts +12 -12
  57. package/src/lib/country-name.ts +59 -59
  58. package/src/lib/get-product-price.ts +133 -133
  59. package/src/lib/media-image.tsx +39 -39
  60. package/src/lib/payment-constants.ts +53 -53
  61. package/src/lib/platform.ts +13 -13
  62. package/src/lib/price.tsx +39 -39
  63. package/src/lib/store-api-error.ts +36 -36
  64. package/src/lib/variant-caption.ts +32 -32
  65. package/src/locales/bg.ts +3 -0
  66. package/src/locales/context.ts +37 -37
  67. package/src/locales/en.ts +26 -26
  68. package/src/locales/es.ts +3 -0
  69. package/src/locales/index.ts +19 -19
  70. package/src/locales/provider.tsx +59 -59
  71. package/src/locales/types.ts +77 -77
  72. package/src/order/index.ts +62 -62
  73. package/src/order/labels.ts +79 -79
  74. package/src/order/order-totals.tsx +250 -250
  75. package/src/primitives/select-field.tsx +93 -93
  76. package/src/products/image-gallery.tsx +43 -43
  77. package/src/products/option-select.tsx +67 -67
  78. package/src/products/product-info.tsx +5 -5
  79. package/src/products/product-price.tsx +69 -69
  80. package/src/products/product-promises.tsx +61 -61
  81. package/src/products/product-specs.tsx +65 -65
  82. package/src/products/product-tabs.tsx +123 -123
  83. package/src/products/purchase-options.tsx +130 -130
  84. package/src/products/sets.ts +110 -110
  85. package/src/products/variant-matching.ts +71 -71
  86. package/src/products/variant-url.ts +74 -74
  87. package/src/reviews-ui/lightbox-state.ts +46 -46
  88. package/src/reviews-ui/review-header.tsx +166 -166
  89. package/src/reviews-ui/review-lightbox.tsx +271 -271
  90. package/src/reviews-ui/review-list.tsx +193 -193
  91. package/src/reviews-ui/review-widget.tsx +219 -218
  92. package/src/reviews-ui/widget-options.ts +55 -55
  93. package/src/store/category-template.tsx +136 -136
  94. package/src/store/index.ts +40 -40
  95. package/src/store/labels.ts +10 -0
  96. package/src/tracking/chatgpt-pixel.tsx +99 -99
  97. package/src/tracking/consent-init.tsx +62 -62
  98. package/src/tracking/events.ts +348 -348
  99. package/src/tracking/ga4.tsx +93 -93
  100. package/src/tracking/google-ads.ts +84 -84
  101. package/src/tracking/gtm.tsx +60 -60
  102. package/src/tracking/inline-script.ts +49 -49
  103. package/src/tracking/oaiq.ts +206 -206
  104. package/src/tracking/tiktok-pixel.tsx +91 -91
  105. package/src/tracking/track-init.tsx +56 -56
  106. package/src/tracking/track-order-purchase.tsx +122 -122
  107. package/src/tracking/ttq.ts +180 -180
  108. package/src/tracking/use-tracking-config.ts +54 -54
  109. package/theme/index.css +25 -25
package/src/lib/price.tsx CHANGED
@@ -1,39 +1,39 @@
1
- import * as React from "react"
2
-
3
- import { convertToLocale } from "./money"
4
-
5
- /**
6
- * Price — render an amount in its own currency, and nothing else.
7
- *
8
- * Replaces `DualPrice` (deleted 2026-09-13, Alexander: "clean and clear
9
- * the dual pricing"). That component appended a grey BGN leg to every EUR
10
- * price for Bulgaria's euro-changeover disclosure rule, with the statutory
11
- * 1.95583 rate baked in. Cartbase prices in EUR (see the EUR-only law) and
12
- * the second currency belongs to a store's own locale work if it ever
13
- * wants one, not to every price in the library.
14
- *
15
- * Stateless and side-effect-free, so it renders in server and client
16
- * components alike, and it is deliberately NOT a client component: the old
17
- * one carried "use client" for no reason, which pulled every price
18
- * rendering into the client bundle.
19
- *
20
- * @example
21
- * <Price amount={19.99} currencyCode="eur" /> // "€19.99"
22
- */
23
-
24
- export type PriceProps = {
25
- /** Amount in major units of the currency (19.99 for €19.99). */
26
- amount: number
27
- /** ISO currency code, case-insensitive. */
28
- currencyCode: string
29
- /** Class for the span. */
30
- className?: string
31
- }
32
-
33
- export function Price({ amount, currencyCode, className }: PriceProps): React.ReactElement {
34
- return (
35
- <span className={className}>
36
- {convertToLocale({ amount, currency_code: currencyCode })}
37
- </span>
38
- )
39
- }
1
+ import * as React from "react"
2
+
3
+ import { convertToLocale } from "./money"
4
+
5
+ /**
6
+ * Price — render an amount in its own currency, and nothing else.
7
+ *
8
+ * Replaces `DualPrice` (deleted 2026-09-13, Alexander: "clean and clear
9
+ * the dual pricing"). That component appended a grey BGN leg to every EUR
10
+ * price for Bulgaria's euro-changeover disclosure rule, with the statutory
11
+ * 1.95583 rate baked in. Cartbase prices in EUR (see the EUR-only law) and
12
+ * the second currency belongs to a store's own locale work if it ever
13
+ * wants one, not to every price in the library.
14
+ *
15
+ * Stateless and side-effect-free, so it renders in server and client
16
+ * components alike, and it is deliberately NOT a client component: the old
17
+ * one carried "use client" for no reason, which pulled every price
18
+ * rendering into the client bundle.
19
+ *
20
+ * @example
21
+ * <Price amount={19.99} currencyCode="eur" /> // "€19.99"
22
+ */
23
+
24
+ export type PriceProps = {
25
+ /** Amount in major units of the currency (19.99 for €19.99). */
26
+ amount: number
27
+ /** ISO currency code, case-insensitive. */
28
+ currencyCode: string
29
+ /** Class for the span. */
30
+ className?: string
31
+ }
32
+
33
+ export function Price({ amount, currencyCode, className }: PriceProps): React.ReactElement {
34
+ return (
35
+ <span className={className}>
36
+ {convertToLocale({ amount, currency_code: currencyCode })}
37
+ </span>
38
+ )
39
+ }
@@ -1,36 +1,36 @@
1
- import { StoreApiError } from "../api/types"
2
-
3
- /**
4
- * storeApiError — the successor of `@1click/ui`'s `medusaError`.
5
- *
6
- * Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
7
- * clean, user-presentable `Error` whose message starts with a capital and
8
- * ends with a period — the same normalization contract the Medusa-era
9
- * helper enforced, adapted to the Cartbase error contract (`StoreApiError`
10
- * with `status` + `code` + raw `body`, see `../api/types`).
11
- *
12
- * Always throws — never returns. Use in `.catch()` chains:
13
- *
14
- * addLineItem(client, cartId, input).catch(storeApiError)
15
- *
16
- * Callers that need the PROGRAMMATIC surface (branching on `status` /
17
- * `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
18
- * `StoreApiError` directly instead — this adapter is the display-boundary
19
- * normalizer, deliberately lossy about everything but the message.
20
- */
21
- export default function storeApiError(error: unknown): never {
22
- if (error instanceof StoreApiError) {
23
- const message = error.message || `Store API error (${error.status})`
24
- throw new Error(sentence(message))
25
- }
26
- if (error instanceof Error) {
27
- throw new Error(sentence(error.message || "Unknown error"))
28
- }
29
- throw new Error("Error setting up the request: " + String(error))
30
- }
31
-
32
- /** Capitalize + terminal period — the medusaError display normalization. */
33
- function sentence(message: string): string {
34
- const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
35
- return capitalized.endsWith(".") ? capitalized : capitalized + "."
36
- }
1
+ import { StoreApiError } from "../api/types"
2
+
3
+ /**
4
+ * storeApiError — the successor of `@1click/ui`'s `medusaError`.
5
+ *
6
+ * Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
7
+ * clean, user-presentable `Error` whose message starts with a capital and
8
+ * ends with a period — the same normalization contract the Medusa-era
9
+ * helper enforced, adapted to the Cartbase error contract (`StoreApiError`
10
+ * with `status` + `code` + raw `body`, see `../api/types`).
11
+ *
12
+ * Always throws — never returns. Use in `.catch()` chains:
13
+ *
14
+ * addLineItem(client, cartId, input).catch(storeApiError)
15
+ *
16
+ * Callers that need the PROGRAMMATIC surface (branching on `status` /
17
+ * `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
18
+ * `StoreApiError` directly instead — this adapter is the display-boundary
19
+ * normalizer, deliberately lossy about everything but the message.
20
+ */
21
+ export default function storeApiError(error: unknown): never {
22
+ if (error instanceof StoreApiError) {
23
+ const message = error.message || `Store API error (${error.status})`
24
+ throw new Error(sentence(message))
25
+ }
26
+ if (error instanceof Error) {
27
+ throw new Error(sentence(error.message || "Unknown error"))
28
+ }
29
+ throw new Error("Error setting up the request: " + String(error))
30
+ }
31
+
32
+ /** Capitalize + terminal period — the medusaError display normalization. */
33
+ function sentence(message: string): string {
34
+ const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
35
+ return capitalized.endsWith(".") ? capitalized : capitalized + "."
36
+ }
@@ -1,32 +1,32 @@
1
- /**
2
- * The variant caption a shopper sees under a product title.
3
- *
4
- * A product with one variant has no real variant, only a placeholder name
5
- * for the row that holds its price: the platform names it "Default", a
6
- * Shopify import carries "Default Title", Medusa "Default variant". None of
7
- * them is a fact about the product, so none is shown. Every surface that
8
- * prints a variant (cart drawer, checkout summary, order page) goes through
9
- * this one function; a real variant title ("Size M / Red") passes through.
10
- *
11
- * Found 2026-09-14 on evoo's checkout summary, where every Shopify-imported
12
- * product read "Default Title" under its name while three surfaces each
13
- * tested a different spelling of the placeholder.
14
- */
15
-
16
- const PLACEHOLDER_TITLES = new Set(["default", "default title", "default variant"])
17
-
18
- /** True for an empty title or any spelling of the single-variant placeholder. */
19
- export function isPlaceholderVariantTitle(
20
- title: string | null | undefined
21
- ): boolean {
22
- if (!title) return true
23
- return PLACEHOLDER_TITLES.has(title.trim().toLowerCase())
24
- }
25
-
26
- /** The caption to render, or null when there is nothing worth showing. */
27
- export function variantCaption(title: string | null | undefined): string | null {
28
- if (!title) return null
29
- const trimmed = title.trim()
30
- if (!trimmed || isPlaceholderVariantTitle(trimmed)) return null
31
- return trimmed
32
- }
1
+ /**
2
+ * The variant caption a shopper sees under a product title.
3
+ *
4
+ * A product with one variant has no real variant, only a placeholder name
5
+ * for the row that holds its price: the platform names it "Default", a
6
+ * Shopify import carries "Default Title", Medusa "Default variant". None of
7
+ * them is a fact about the product, so none is shown. Every surface that
8
+ * prints a variant (cart drawer, checkout summary, order page) goes through
9
+ * this one function; a real variant title ("Size M / Red") passes through.
10
+ *
11
+ * Found 2026-09-14 on evoo's checkout summary, where every Shopify-imported
12
+ * product read "Default Title" under its name while three surfaces each
13
+ * tested a different spelling of the placeholder.
14
+ */
15
+
16
+ const PLACEHOLDER_TITLES = new Set(["default", "default title", "default variant"])
17
+
18
+ /** True for an empty title or any spelling of the single-variant placeholder. */
19
+ export function isPlaceholderVariantTitle(
20
+ title: string | null | undefined
21
+ ): boolean {
22
+ if (!title) return true
23
+ return PLACEHOLDER_TITLES.has(title.trim().toLowerCase())
24
+ }
25
+
26
+ /** The caption to render, or null when there is nothing worth showing. */
27
+ export function variantCaption(title: string | null | undefined): string | null {
28
+ if (!title) return null
29
+ const trimmed = title.trim()
30
+ if (!trimmed || isPlaceholderVariantTitle(trimmed)) return null
31
+ return trimmed
32
+ }
package/src/locales/bg.ts CHANGED
@@ -60,6 +60,9 @@ export const bg: StorefrontLocale = {
60
60
  noResults: "Няма намерени продукти",
61
61
  filters: "Филтри",
62
62
  clearFilters: "Изчисти филтрите",
63
+ poweredBy: "Създаден с",
64
+ paymentMethods: "Начини на плащане",
65
+ socialProfiles: "Последвайте ни",
63
66
  },
64
67
  cartDrawer: {
65
68
  yourCart: "Вашата кошница",
@@ -1,37 +1,37 @@
1
- "use client"
2
-
3
- import { createContext, useContext } from "react"
4
- import type { StorefrontLocale } from "./types"
5
-
6
- /**
7
- * THE MOUNTED LANGUAGE, as a context. Null outside `StorefrontLocaleProvider`.
8
- *
9
- * Its default is null and not `en` on purpose: `en` gathers every area's
10
- * English, and a client component that needs one area (the cart drawer, a
11
- * review card) must not bundle the other five to read it. Each reader hands
12
- * over its own area's English instead, through `useLocaleArea`, so a
13
- * component rendered with no language mounted behaves exactly as before.
14
- */
15
- export const StorefrontLocaleContext = createContext<StorefrontLocale | null>(null)
16
-
17
- /**
18
- * One area of the mounted language, for a CLIENT component that renders it.
19
- *
20
- * const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
21
- *
22
- * The pack the store mounted wins over the area's English; a `labels` prop
23
- * still wins over both, so a store can override one word without a pack.
24
- * Since 2026-09-14 this is how the cart drawer and the reviews family are
25
- * localized: mount the provider once and there is nothing left to forget.
26
- *
27
- * A SERVER component cannot call this, because a server component has no
28
- * context. That is why the store, product and order templates take their
29
- * pack as a REQUIRED prop instead (`PropLabelAreas` in ./types).
30
- */
31
- export function useLocaleArea<K extends keyof StorefrontLocale>(
32
- area: K,
33
- english: StorefrontLocale[K]
34
- ): StorefrontLocale[K] {
35
- const locale = useContext(StorefrontLocaleContext)
36
- return locale ? locale[area] : english
37
- }
1
+ "use client"
2
+
3
+ import { createContext, useContext } from "react"
4
+ import type { StorefrontLocale } from "./types"
5
+
6
+ /**
7
+ * THE MOUNTED LANGUAGE, as a context. Null outside `StorefrontLocaleProvider`.
8
+ *
9
+ * Its default is null and not `en` on purpose: `en` gathers every area's
10
+ * English, and a client component that needs one area (the cart drawer, a
11
+ * review card) must not bundle the other five to read it. Each reader hands
12
+ * over its own area's English instead, through `useLocaleArea`, so a
13
+ * component rendered with no language mounted behaves exactly as before.
14
+ */
15
+ export const StorefrontLocaleContext = createContext<StorefrontLocale | null>(null)
16
+
17
+ /**
18
+ * One area of the mounted language, for a CLIENT component that renders it.
19
+ *
20
+ * const l = { ...useLocaleArea("reviews", defaultReviewsUiLabels), ...labels }
21
+ *
22
+ * The pack the store mounted wins over the area's English; a `labels` prop
23
+ * still wins over both, so a store can override one word without a pack.
24
+ * Since 2026-09-14 this is how the cart drawer and the reviews family are
25
+ * localized: mount the provider once and there is nothing left to forget.
26
+ *
27
+ * A SERVER component cannot call this, because a server component has no
28
+ * context. That is why the store, product and order templates take their
29
+ * pack as a REQUIRED prop instead (`PropLabelAreas` in ./types).
30
+ */
31
+ export function useLocaleArea<K extends keyof StorefrontLocale>(
32
+ area: K,
33
+ english: StorefrontLocale[K]
34
+ ): StorefrontLocale[K] {
35
+ const locale = useContext(StorefrontLocaleContext)
36
+ return locale ? locale[area] : english
37
+ }
package/src/locales/en.ts CHANGED
@@ -1,26 +1,26 @@
1
- import { defaultCartDrawerLabels } from "../cart-drawer/labels"
2
- import { defaultCheckoutLabels } from "../checkout/labels"
3
- import { defaultOrderLabels } from "../order/labels"
4
- import { defaultProductLabels } from "../products/labels"
5
- import { defaultReviewsUiLabels } from "../reviews-ui/labels"
6
- import { defaultStoreLabels } from "../store/labels"
7
- import type { StorefrontLocale } from "./types"
8
-
9
- /**
10
- * ENGLISH — the library's own default, gathered into one locale object.
11
- *
12
- * It holds NO copy of its own: every field points at the defaults that
13
- * live beside each area's type, which are what a component renders when no
14
- * provider is mounted. So English cannot drift from the library, and a new
15
- * label added to any area appears here with no edit.
16
- */
17
- export const en: StorefrontLocale = {
18
- code: "en",
19
- name: "English",
20
- products: defaultProductLabels,
21
- store: defaultStoreLabels,
22
- cartDrawer: defaultCartDrawerLabels,
23
- checkout: defaultCheckoutLabels,
24
- order: defaultOrderLabels,
25
- reviews: defaultReviewsUiLabels,
26
- }
1
+ import { defaultCartDrawerLabels } from "../cart-drawer/labels"
2
+ import { defaultCheckoutLabels } from "../checkout/labels"
3
+ import { defaultOrderLabels } from "../order/labels"
4
+ import { defaultProductLabels } from "../products/labels"
5
+ import { defaultReviewsUiLabels } from "../reviews-ui/labels"
6
+ import { defaultStoreLabels } from "../store/labels"
7
+ import type { StorefrontLocale } from "./types"
8
+
9
+ /**
10
+ * ENGLISH — the library's own default, gathered into one locale object.
11
+ *
12
+ * It holds NO copy of its own: every field points at the defaults that
13
+ * live beside each area's type, which are what a component renders when no
14
+ * provider is mounted. So English cannot drift from the library, and a new
15
+ * label added to any area appears here with no edit.
16
+ */
17
+ export const en: StorefrontLocale = {
18
+ code: "en",
19
+ name: "English",
20
+ products: defaultProductLabels,
21
+ store: defaultStoreLabels,
22
+ cartDrawer: defaultCartDrawerLabels,
23
+ checkout: defaultCheckoutLabels,
24
+ order: defaultOrderLabels,
25
+ reviews: defaultReviewsUiLabels,
26
+ }
package/src/locales/es.ts CHANGED
@@ -65,6 +65,9 @@ export const es: StorefrontLocale = {
65
65
  noResults: "No se han encontrado productos",
66
66
  filters: "Filtros",
67
67
  clearFilters: "Borrar filtros",
68
+ poweredBy: "Con la tecnología de",
69
+ paymentMethods: "Métodos de pago",
70
+ socialProfiles: "Síguenos",
68
71
  },
69
72
  cartDrawer: {
70
73
  yourCart: "Tu carrito",
@@ -1,19 +1,19 @@
1
- /**
2
- * THE LOCALE LAYER.
3
- *
4
- * The library speaks international ENGLISH by default and knows nothing
5
- * about any other language. A language is a PACK the store imports:
6
- *
7
- * import { es } from "@cartbase/storefront/locales/es"
8
- * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
9
- *
10
- * Import a pack by its own subpath (`/locales/es`, `/locales/bg`) rather
11
- * than from here, so a store that ships one language does not bundle the
12
- * others. This barrel carries the type, the provider and English only.
13
- *
14
- * Shipping: English, Spanish, Bulgarian (2026-09-13). Adding a language is
15
- * one file in this folder plus its export path, and nothing else.
16
- */
17
- export { en } from "./en"
18
- export { StorefrontLocaleProvider, useStorefrontLocale } from "./provider"
19
- export type { PropLabelAreas, StorefrontLocale } from "./types"
1
+ /**
2
+ * THE LOCALE LAYER.
3
+ *
4
+ * The library speaks international ENGLISH by default and knows nothing
5
+ * about any other language. A language is a PACK the store imports:
6
+ *
7
+ * import { es } from "@cartbase/storefront/locales/es"
8
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
9
+ *
10
+ * Import a pack by its own subpath (`/locales/es`, `/locales/bg`) rather
11
+ * than from here, so a store that ships one language does not bundle the
12
+ * others. This barrel carries the type, the provider and English only.
13
+ *
14
+ * Shipping: English, Spanish, Bulgarian (2026-09-13). Adding a language is
15
+ * one file in this folder plus its export path, and nothing else.
16
+ */
17
+ export { en } from "./en"
18
+ export { StorefrontLocaleProvider, useStorefrontLocale } from "./provider"
19
+ export type { PropLabelAreas, StorefrontLocale } from "./types"
@@ -1,59 +1,59 @@
1
- "use client"
2
-
3
- import { useContext, type ReactNode } from "react"
4
- import { CheckoutProvider } from "../checkout/context"
5
- import { OrderLabelsProvider } from "../order/context"
6
- import { ProductLabelsProvider } from "../products/context"
7
- import { StorefrontLocaleContext } from "./context"
8
- import type { StorefrontLocale } from "./types"
9
- import { en } from "./en"
10
-
11
- /**
12
- * ONE PROVIDER FOR THE STORE'S LANGUAGE.
13
- *
14
- * import { es } from "@cartbase/storefront/locales/es"
15
- * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
16
- *
17
- * Every CLIENT component inside it speaks the pack, and nothing else has to
18
- * be wired: it mounts the three label contexts (products, checkout, order),
19
- * and the cart drawer and the reviews family read the language from it
20
- * directly (`useLocaleArea`, since 2026-09-14). It does not MOUNT the cart
21
- * drawer, because that provider carries the cart's state, its client and its
22
- * persistence callbacks, which belong where the app owns that state; the
23
- * drawer only reads the language from here.
24
- *
25
- * What it cannot reach is a SERVER component, because a server component has
26
- * no context. The store, product and order templates render on the server,
27
- * so each page hands them their area of the pack as a REQUIRED prop:
28
- * `PropLabelAreas` in ./types names them, with the mounts written out.
29
- *
30
- * DEFAULT IS ENGLISH. A component rendered outside this provider reads the
31
- * library's own English defaults, so nothing renders blank or crashes for
32
- * want of a provider.
33
- */
34
- export function StorefrontLocaleProvider({
35
- locale,
36
- children,
37
- }: {
38
- locale: StorefrontLocale
39
- children: ReactNode
40
- }) {
41
- return (
42
- <StorefrontLocaleContext.Provider value={locale}>
43
- <ProductLabelsProvider labels={locale.products}>
44
- <CheckoutProvider labels={locale.checkout}>
45
- <OrderLabelsProvider labels={locale.order}>{children}</OrderLabelsProvider>
46
- </CheckoutProvider>
47
- </ProductLabelsProvider>
48
- </StorefrontLocaleContext.Provider>
49
- )
50
- }
51
-
52
- /**
53
- * The whole mounted locale, English when none is mounted: for the code that
54
- * needs the language itself, e.g. `<html lang={locale.code}>`, and for a
55
- * client shell that hands a server template its pack.
56
- */
57
- export function useStorefrontLocale(): StorefrontLocale {
58
- return useContext(StorefrontLocaleContext) ?? en
59
- }
1
+ "use client"
2
+
3
+ import { useContext, type ReactNode } from "react"
4
+ import { CheckoutProvider } from "../checkout/context"
5
+ import { OrderLabelsProvider } from "../order/context"
6
+ import { ProductLabelsProvider } from "../products/context"
7
+ import { StorefrontLocaleContext } from "./context"
8
+ import type { StorefrontLocale } from "./types"
9
+ import { en } from "./en"
10
+
11
+ /**
12
+ * ONE PROVIDER FOR THE STORE'S LANGUAGE.
13
+ *
14
+ * import { es } from "@cartbase/storefront/locales/es"
15
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
16
+ *
17
+ * Every CLIENT component inside it speaks the pack, and nothing else has to
18
+ * be wired: it mounts the three label contexts (products, checkout, order),
19
+ * and the cart drawer and the reviews family read the language from it
20
+ * directly (`useLocaleArea`, since 2026-09-14). It does not MOUNT the cart
21
+ * drawer, because that provider carries the cart's state, its client and its
22
+ * persistence callbacks, which belong where the app owns that state; the
23
+ * drawer only reads the language from here.
24
+ *
25
+ * What it cannot reach is a SERVER component, because a server component has
26
+ * no context. The store, product and order templates render on the server,
27
+ * so each page hands them their area of the pack as a REQUIRED prop:
28
+ * `PropLabelAreas` in ./types names them, with the mounts written out.
29
+ *
30
+ * DEFAULT IS ENGLISH. A component rendered outside this provider reads the
31
+ * library's own English defaults, so nothing renders blank or crashes for
32
+ * want of a provider.
33
+ */
34
+ export function StorefrontLocaleProvider({
35
+ locale,
36
+ children,
37
+ }: {
38
+ locale: StorefrontLocale
39
+ children: ReactNode
40
+ }) {
41
+ return (
42
+ <StorefrontLocaleContext.Provider value={locale}>
43
+ <ProductLabelsProvider labels={locale.products}>
44
+ <CheckoutProvider labels={locale.checkout}>
45
+ <OrderLabelsProvider labels={locale.order}>{children}</OrderLabelsProvider>
46
+ </CheckoutProvider>
47
+ </ProductLabelsProvider>
48
+ </StorefrontLocaleContext.Provider>
49
+ )
50
+ }
51
+
52
+ /**
53
+ * The whole mounted locale, English when none is mounted: for the code that
54
+ * needs the language itself, e.g. `<html lang={locale.code}>`, and for a
55
+ * client shell that hands a server template its pack.
56
+ */
57
+ export function useStorefrontLocale(): StorefrontLocale {
58
+ return useContext(StorefrontLocaleContext) ?? en
59
+ }