@cartbase/storefront 0.16.0 → 0.18.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 (110) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -255
  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 +180 -130
  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/products.ts +43 -1
  18. package/src/api/redirects.ts +37 -37
  19. package/src/api/regions.ts +200 -200
  20. package/src/api/reviews.ts +259 -259
  21. package/src/api/search.ts +163 -163
  22. package/src/api/store.ts +35 -35
  23. package/src/api/types.ts +91 -91
  24. package/src/cart-drawer/context.tsx +277 -164
  25. package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
  26. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
  27. package/src/cart-drawer/free-gift.tsx +1 -1
  28. package/src/cart-drawer/gift-wrap.tsx +82 -82
  29. package/src/cart-drawer/index.ts +71 -68
  30. package/src/cart-drawer/item/index.tsx +162 -162
  31. package/src/cart-drawer/item/quantity.tsx +8 -14
  32. package/src/cart-drawer/item/upsell.tsx +110 -110
  33. package/src/cart-drawer/labels.ts +123 -123
  34. package/src/cart-drawer/mutation-queue.ts +78 -0
  35. package/src/cart-drawer/notes.tsx +131 -131
  36. package/src/cart-drawer/sticky-footer.tsx +73 -73
  37. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  38. package/src/checkout/address-error-copy.ts +117 -117
  39. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  40. package/src/checkout/compare-addresses.ts +40 -40
  41. package/src/checkout/discount-section.tsx +218 -218
  42. package/src/checkout/error-copy-codes.ts +63 -63
  43. package/src/checkout/geocode.ts +154 -154
  44. package/src/checkout/gift-card-section.tsx +224 -224
  45. package/src/checkout/index.ts +72 -72
  46. package/src/checkout/labels.ts +495 -495
  47. package/src/checkout/payment-button.tsx +372 -372
  48. package/src/checkout/payment-error-copy.ts +154 -154
  49. package/src/checkout/promotion-error-copy.ts +91 -91
  50. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  51. package/src/common/country-flag.tsx +52 -52
  52. package/src/common/country-select.tsx +11 -11
  53. package/src/common/index.ts +20 -20
  54. package/src/common/market-select.tsx +57 -57
  55. package/src/index.ts +12 -12
  56. package/src/lib/country-name.ts +59 -59
  57. package/src/lib/get-product-price.ts +133 -133
  58. package/src/lib/media-image.tsx +39 -0
  59. package/src/lib/payment-constants.ts +53 -53
  60. package/src/lib/platform.ts +13 -13
  61. package/src/lib/price.tsx +39 -39
  62. package/src/lib/store-api-error.ts +36 -36
  63. package/src/lib/variant-caption.ts +32 -32
  64. package/src/locales/bg.ts +467 -467
  65. package/src/locales/context.ts +37 -37
  66. package/src/locales/en.ts +26 -26
  67. package/src/locales/es.ts +466 -466
  68. package/src/locales/index.ts +19 -19
  69. package/src/locales/provider.tsx +59 -59
  70. package/src/locales/types.ts +77 -77
  71. package/src/order/index.ts +62 -62
  72. package/src/order/labels.ts +79 -79
  73. package/src/order/order-item.tsx +1 -1
  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/index.ts +68 -59
  78. package/src/products/option-select.tsx +67 -67
  79. package/src/products/product-actions-wrapper.tsx +63 -62
  80. package/src/products/product-actions.tsx +151 -150
  81. package/src/products/product-price.tsx +69 -69
  82. package/src/products/product-promises.tsx +61 -61
  83. package/src/products/product-specs.tsx +65 -65
  84. package/src/products/product-tabs.tsx +123 -123
  85. package/src/products/product-template.tsx +163 -162
  86. package/src/products/purchase-options.tsx +130 -130
  87. package/src/products/sets.ts +110 -0
  88. package/src/products/thumbnail.tsx +1 -1
  89. package/src/products/use-product-actions.ts +199 -164
  90. package/src/products/variant-matching.ts +71 -71
  91. package/src/products/variant-url.ts +74 -74
  92. package/src/reviews-ui/index.ts +73 -73
  93. package/src/store/category-template.tsx +136 -136
  94. package/src/store/index.ts +40 -40
  95. package/src/tracking/chatgpt-pixel.tsx +99 -99
  96. package/src/tracking/consent-init.tsx +62 -62
  97. package/src/tracking/events.ts +348 -348
  98. package/src/tracking/ga4.tsx +93 -93
  99. package/src/tracking/google-ads.ts +84 -84
  100. package/src/tracking/gtm.tsx +60 -60
  101. package/src/tracking/inline-script.ts +49 -49
  102. package/src/tracking/oaiq.ts +206 -206
  103. package/src/tracking/tiktok-pixel.tsx +91 -91
  104. package/src/tracking/track-init.tsx +56 -56
  105. package/src/tracking/track-order-purchase.tsx +122 -122
  106. package/src/tracking/ttq.ts +180 -180
  107. package/src/tracking/use-tracking-config.ts +54 -54
  108. package/theme/index.css +25 -25
  109. package/theme/theme.css +150 -150
  110. package/theme/tokens.css +106 -106
@@ -1,133 +1,133 @@
1
- import { getPercentageDiff } from "./get-percentage-diff"
2
- import { convertToLocale } from "./money"
3
-
4
- /**
5
- * Product price extraction — ported from
6
- * `@1click/ui/src/lib/get-product-price.ts` (v2.3.1). The
7
- * `@medusajs/types` `HttpTypes.StoreProduct` parameter is replaced by the
8
- * structural shapes below so this module stays decoupled from the SDK
9
- * domain modules (Cartbase's `variant.calculated_price` is the b2b-v1
10
- * pricing context documented in `@cartbase/storefront/api/types`
11
- * `CalculatedPrice`, decimal EUR major units).
12
- *
13
- * Wire-shape note (code truth, `src/lib/products/pricing.ts`): Cartbase serves
14
- * `calculated_price.price_list_type` FLAT on the calculated_price object.
15
- * The Medusa wire shape ALSO nests a `calculated_price.calculated_price`
16
- * detail object carrying `price_list_type` — the original library read the
17
- * nested path. `getPricesForVariant` reads the flat key first and falls
18
- * back to the nested path so both shapes produce the same result.
19
- */
20
-
21
- /** Structural `variant.calculated_price` shape (see api/types CalculatedPrice). */
22
- export type CalculatedPriceLike = {
23
- calculated_amount: number
24
- original_amount: number
25
- currency_code: string
26
- price_list_type?: string | null
27
- /** Medusa-wire nested detail object (fallback path). */
28
- calculated_price?: { price_list_type?: string | null } | null
29
- }
30
-
31
- /** Structural variant shape — any richer product-variant DTO satisfies it. */
32
- export type VariantWithPrice = {
33
- id?: string
34
- sku?: string | null
35
- calculated_price?: CalculatedPriceLike | null
36
- }
37
-
38
- /** Structural product shape — any richer product DTO satisfies it. */
39
- export type ProductWithVariants = {
40
- id?: string
41
- variants?: VariantWithPrice[] | null
42
- }
43
-
44
- export type VariantPrice = {
45
- calculated_price_number: number
46
- calculated_price: string
47
- original_price_number: number
48
- original_price: string
49
- currency_code: string
50
- price_type: string | null | undefined
51
- percentage_diff: string
52
- }
53
-
54
- export const getPricesForVariant = (
55
- variant: VariantWithPrice | null | undefined
56
- ): VariantPrice | null => {
57
- if (!variant?.calculated_price?.calculated_amount) {
58
- return null
59
- }
60
-
61
- const cp = variant.calculated_price
62
- return {
63
- calculated_price_number: cp.calculated_amount,
64
- calculated_price: convertToLocale({
65
- amount: cp.calculated_amount,
66
- currency_code: cp.currency_code,
67
- }),
68
- original_price_number: cp.original_amount,
69
- original_price: convertToLocale({
70
- amount: cp.original_amount,
71
- currency_code: cp.currency_code,
72
- }),
73
- currency_code: cp.currency_code,
74
- // Flat key (Cartbase wire shape) first — an explicit null stays null;
75
- // the nested Medusa detail is the fallback only when the flat key is
76
- // absent entirely (the original @1click read path).
77
- price_type:
78
- cp.price_list_type !== undefined
79
- ? cp.price_list_type
80
- : cp.calculated_price?.price_list_type,
81
- percentage_diff: getPercentageDiff(cp.original_amount, cp.calculated_amount),
82
- }
83
- }
84
-
85
- export function getProductPrice({
86
- product,
87
- variantId,
88
- }: {
89
- product: ProductWithVariants
90
- variantId?: string
91
- }) {
92
- if (!product || !product.id) {
93
- throw new Error("No product provided")
94
- }
95
-
96
- const cheapestPrice = (): VariantPrice | null => {
97
- if (!product || !product.variants?.length) {
98
- return null
99
- }
100
-
101
- const cheapestVariant = product.variants
102
- .filter((v) => !!v.calculated_price)
103
- .sort(
104
- (a, b) =>
105
- (a.calculated_price?.calculated_amount ?? 0) -
106
- (b.calculated_price?.calculated_amount ?? 0)
107
- )[0]
108
-
109
- return getPricesForVariant(cheapestVariant)
110
- }
111
-
112
- const variantPrice = (): VariantPrice | null => {
113
- if (!product || !variantId) {
114
- return null
115
- }
116
-
117
- const variant = product.variants?.find(
118
- (v) => v.id === variantId || v.sku === variantId
119
- )
120
-
121
- if (!variant) {
122
- return null
123
- }
124
-
125
- return getPricesForVariant(variant)
126
- }
127
-
128
- return {
129
- product,
130
- cheapestPrice: cheapestPrice(),
131
- variantPrice: variantPrice(),
132
- }
133
- }
1
+ import { getPercentageDiff } from "./get-percentage-diff"
2
+ import { convertToLocale } from "./money"
3
+
4
+ /**
5
+ * Product price extraction — ported from
6
+ * `@1click/ui/src/lib/get-product-price.ts` (v2.3.1). The
7
+ * `@medusajs/types` `HttpTypes.StoreProduct` parameter is replaced by the
8
+ * structural shapes below so this module stays decoupled from the SDK
9
+ * domain modules (Cartbase's `variant.calculated_price` is the b2b-v1
10
+ * pricing context documented in `@cartbase/storefront/api/types`
11
+ * `CalculatedPrice`, decimal EUR major units).
12
+ *
13
+ * Wire-shape note (code truth, `src/lib/products/pricing.ts`): Cartbase serves
14
+ * `calculated_price.price_list_type` FLAT on the calculated_price object.
15
+ * The Medusa wire shape ALSO nests a `calculated_price.calculated_price`
16
+ * detail object carrying `price_list_type` — the original library read the
17
+ * nested path. `getPricesForVariant` reads the flat key first and falls
18
+ * back to the nested path so both shapes produce the same result.
19
+ */
20
+
21
+ /** Structural `variant.calculated_price` shape (see api/types CalculatedPrice). */
22
+ export type CalculatedPriceLike = {
23
+ calculated_amount: number
24
+ original_amount: number
25
+ currency_code: string
26
+ price_list_type?: string | null
27
+ /** Medusa-wire nested detail object (fallback path). */
28
+ calculated_price?: { price_list_type?: string | null } | null
29
+ }
30
+
31
+ /** Structural variant shape — any richer product-variant DTO satisfies it. */
32
+ export type VariantWithPrice = {
33
+ id?: string
34
+ sku?: string | null
35
+ calculated_price?: CalculatedPriceLike | null
36
+ }
37
+
38
+ /** Structural product shape — any richer product DTO satisfies it. */
39
+ export type ProductWithVariants = {
40
+ id?: string
41
+ variants?: VariantWithPrice[] | null
42
+ }
43
+
44
+ export type VariantPrice = {
45
+ calculated_price_number: number
46
+ calculated_price: string
47
+ original_price_number: number
48
+ original_price: string
49
+ currency_code: string
50
+ price_type: string | null | undefined
51
+ percentage_diff: string
52
+ }
53
+
54
+ export const getPricesForVariant = (
55
+ variant: VariantWithPrice | null | undefined
56
+ ): VariantPrice | null => {
57
+ if (!variant?.calculated_price?.calculated_amount) {
58
+ return null
59
+ }
60
+
61
+ const cp = variant.calculated_price
62
+ return {
63
+ calculated_price_number: cp.calculated_amount,
64
+ calculated_price: convertToLocale({
65
+ amount: cp.calculated_amount,
66
+ currency_code: cp.currency_code,
67
+ }),
68
+ original_price_number: cp.original_amount,
69
+ original_price: convertToLocale({
70
+ amount: cp.original_amount,
71
+ currency_code: cp.currency_code,
72
+ }),
73
+ currency_code: cp.currency_code,
74
+ // Flat key (Cartbase wire shape) first — an explicit null stays null;
75
+ // the nested Medusa detail is the fallback only when the flat key is
76
+ // absent entirely (the original @1click read path).
77
+ price_type:
78
+ cp.price_list_type !== undefined
79
+ ? cp.price_list_type
80
+ : cp.calculated_price?.price_list_type,
81
+ percentage_diff: getPercentageDiff(cp.original_amount, cp.calculated_amount),
82
+ }
83
+ }
84
+
85
+ export function getProductPrice({
86
+ product,
87
+ variantId,
88
+ }: {
89
+ product: ProductWithVariants
90
+ variantId?: string
91
+ }) {
92
+ if (!product || !product.id) {
93
+ throw new Error("No product provided")
94
+ }
95
+
96
+ const cheapestPrice = (): VariantPrice | null => {
97
+ if (!product || !product.variants?.length) {
98
+ return null
99
+ }
100
+
101
+ const cheapestVariant = product.variants
102
+ .filter((v) => !!v.calculated_price)
103
+ .sort(
104
+ (a, b) =>
105
+ (a.calculated_price?.calculated_amount ?? 0) -
106
+ (b.calculated_price?.calculated_amount ?? 0)
107
+ )[0]
108
+
109
+ return getPricesForVariant(cheapestVariant)
110
+ }
111
+
112
+ const variantPrice = (): VariantPrice | null => {
113
+ if (!product || !variantId) {
114
+ return null
115
+ }
116
+
117
+ const variant = product.variants?.find(
118
+ (v) => v.id === variantId || v.sku === variantId
119
+ )
120
+
121
+ if (!variant) {
122
+ return null
123
+ }
124
+
125
+ return getPricesForVariant(variant)
126
+ }
127
+
128
+ return {
129
+ product,
130
+ cheapestPrice: cheapestPrice(),
131
+ variantPrice: variantPrice(),
132
+ }
133
+ }
@@ -0,0 +1,39 @@
1
+ import NextImage, { type ImageProps } from "next/image"
2
+
3
+ /**
4
+ * The store's picture element (store-speed-law card, step 3, 2026-09-15).
5
+ *
6
+ * Product pictures live on the Cartbase media CDN, and Next resizes them on
7
+ * the store's own project, per device and in the modern formats, which the
8
+ * scaffold allows through `images.remotePatterns` (next.config.ts). The
9
+ * scaffold used to ship `images.unoptimized: true` instead, so no picture
10
+ * was ever resized: a phone downloaded the merchant's full upload.
11
+ *
12
+ * A picture from any other host (a catalogue not yet rehosted, a demo
13
+ * placeholder, a merchant's own asset host) renders as it is instead of
14
+ * failing the page: Next refuses to resize a host the config does not
15
+ * list, so this element switches resizing off for those. A merchant who
16
+ * adds their host to `remotePatterns` gets resizing there too by passing
17
+ * `unoptimized={false}` where they render it, or by using `next/image`
18
+ * directly.
19
+ */
20
+
21
+ /** The public base of the media CDN (REGISTRY.md, CDN domain). */
22
+ export const MEDIA_HOST = "cartbase.cloud"
23
+
24
+ /** Is this picture served by the media CDN (or by the app itself)? */
25
+ export function isResizableSrc(src: ImageProps["src"]): boolean {
26
+ if (typeof src !== "string") return true
27
+ if (src.startsWith("/")) return true
28
+ try {
29
+ const host = new URL(src).hostname
30
+ return host === MEDIA_HOST || host.endsWith(`.${MEDIA_HOST}`)
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ export function StoreImage(props: ImageProps) {
37
+ const unoptimized = props.unoptimized ?? !isResizableSrc(props.src)
38
+ return <NextImage {...props} unoptimized={unoptimized} />
39
+ }
@@ -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
+ }
@@ -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
+ }