@cartbase/storefront 0.20.0 → 0.20.1
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.
- package/LICENSE +21 -21
- package/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payment tender helpers — identifier sniffing for store payment sessions.
|
|
3
|
-
*
|
|
4
|
-
* The Cartbase wire has exactly TWO tender shapes since the pp_* kill
|
|
5
|
-
* (20260811250000, no-payment-method-by-default card):
|
|
6
|
-
*
|
|
7
|
-
* - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
|
|
8
|
-
* today; PayPal etc. as integrations land);
|
|
9
|
-
* - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
|
|
10
|
-
* `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
|
|
11
|
-
* collection-on-delivery switch). Its session carries `provider_id`
|
|
12
|
-
* NULL; the merchant's NAME is the display everywhere.
|
|
13
|
-
*
|
|
14
|
-
* The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
|
|
15
|
-
* the wire and from this module. The Medusa-era Stripe prefixes
|
|
16
|
-
* (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
|
|
17
|
-
* ported later keep working against either wire.
|
|
18
|
-
*
|
|
19
|
-
* Intentionally ships NO icons — stores render their own icons via their
|
|
20
|
-
* own icon set. The library stays free of icon-kit imports at this layer.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* True if the provider id is a Stripe-backed card payment provider.
|
|
25
|
-
* Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
|
|
26
|
-
* Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
|
|
27
|
-
*/
|
|
28
|
-
export const isStripeLike = (providerId?: string | null): boolean => {
|
|
29
|
-
return Boolean(
|
|
30
|
-
providerId &&
|
|
31
|
-
(providerId === "pp_stripe" ||
|
|
32
|
-
providerId === "stripe" ||
|
|
33
|
-
providerId.startsWith("pp_stripe_") ||
|
|
34
|
-
providerId.startsWith("pp_medusa-"))
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** True if the provider id is PayPal. */
|
|
39
|
-
export const isPaypal = (providerId?: string | null): boolean => {
|
|
40
|
-
return Boolean(providerId?.startsWith("pp_paypal"))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const paymentInfoMap: Record<
|
|
44
|
-
string,
|
|
45
|
-
{ title: string; icon: React.JSX.Element | null }
|
|
46
|
-
> = {
|
|
47
|
-
pp_stripe: { title: "Credit / debit card", icon: null },
|
|
48
|
-
pp_stripe_stripe: { title: "Credit / debit card", icon: null },
|
|
49
|
-
"pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
|
|
50
|
-
"pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
|
|
51
|
-
"pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
|
|
52
|
-
pp_paypal_paypal: { title: "PayPal", icon: null },
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Payment tender helpers — identifier sniffing for store payment sessions.
|
|
3
|
+
*
|
|
4
|
+
* The Cartbase wire has exactly TWO tender shapes since the pp_* kill
|
|
5
|
+
* (20260811250000, no-payment-method-by-default card):
|
|
6
|
+
*
|
|
7
|
+
* - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
|
|
8
|
+
* today; PayPal etc. as integrations land);
|
|
9
|
+
* - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
|
|
10
|
+
* `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
|
|
11
|
+
* collection-on-delivery switch). Its session carries `provider_id`
|
|
12
|
+
* NULL; the merchant's NAME is the display everywhere.
|
|
13
|
+
*
|
|
14
|
+
* The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
|
|
15
|
+
* the wire and from this module. The Medusa-era Stripe prefixes
|
|
16
|
+
* (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
|
|
17
|
+
* ported later keep working against either wire.
|
|
18
|
+
*
|
|
19
|
+
* Intentionally ships NO icons — stores render their own icons via their
|
|
20
|
+
* own icon set. The library stays free of icon-kit imports at this layer.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True if the provider id is a Stripe-backed card payment provider.
|
|
25
|
+
* Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
|
|
26
|
+
* Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
|
|
27
|
+
*/
|
|
28
|
+
export const isStripeLike = (providerId?: string | null): boolean => {
|
|
29
|
+
return Boolean(
|
|
30
|
+
providerId &&
|
|
31
|
+
(providerId === "pp_stripe" ||
|
|
32
|
+
providerId === "stripe" ||
|
|
33
|
+
providerId.startsWith("pp_stripe_") ||
|
|
34
|
+
providerId.startsWith("pp_medusa-"))
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** True if the provider id is PayPal. */
|
|
39
|
+
export const isPaypal = (providerId?: string | null): boolean => {
|
|
40
|
+
return Boolean(providerId?.startsWith("pp_paypal"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const paymentInfoMap: Record<
|
|
44
|
+
string,
|
|
45
|
+
{ title: string; icon: React.JSX.Element | null }
|
|
46
|
+
> = {
|
|
47
|
+
pp_stripe: { title: "Credit / debit card", icon: null },
|
|
48
|
+
pp_stripe_stripe: { title: "Credit / debit card", icon: null },
|
|
49
|
+
"pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
|
|
50
|
+
"pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
|
|
51
|
+
"pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
|
|
52
|
+
pp_paypal_paypal: { title: "PayPal", icon: null },
|
|
53
|
+
}
|
package/src/lib/platform.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
|
|
3
|
-
* a constant, not an input. Every Cartbase store's API lives here; a
|
|
4
|
-
* storefront overrides it only to point at a local or staging platform
|
|
5
|
-
* (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
|
|
6
|
-
* publishable key does.
|
|
7
|
-
*/
|
|
8
|
-
export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
|
|
9
|
-
|
|
10
|
-
/** The origin to call: the override when set, else the platform. */
|
|
11
|
-
export function cartbaseApiOrigin(override?: string | null): string {
|
|
12
|
-
return (override && override.trim()) || CARTBASE_API_ORIGIN
|
|
13
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
|
|
3
|
+
* a constant, not an input. Every Cartbase store's API lives here; a
|
|
4
|
+
* storefront overrides it only to point at a local or staging platform
|
|
5
|
+
* (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
|
|
6
|
+
* publishable key does.
|
|
7
|
+
*/
|
|
8
|
+
export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
|
|
9
|
+
|
|
10
|
+
/** The origin to call: the override when set, else the platform. */
|
|
11
|
+
export function cartbaseApiOrigin(override?: string | null): string {
|
|
12
|
+
return (override && override.trim()) || CARTBASE_API_ORIGIN
|
|
13
|
+
}
|
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/context.ts
CHANGED
|
@@ -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/index.ts
CHANGED
|
@@ -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"
|
package/src/locales/provider.tsx
CHANGED
|
@@ -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
|
+
}
|