@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
@@ -1,93 +1,93 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
-
5
- import { cn } from "../lib/utils"
6
-
7
- /**
8
- * SelectField — floating-label native select primitive.
9
- *
10
- * Matches `Field`'s visual treatment — the label floats into the top-left
11
- * when a value is selected. Uses a plain native `<select>` for simplicity
12
- * and native mobile keyboard support. If a fancier dropdown is needed
13
- * (searchable, multi-select, rich options), use the shadcn `Select`
14
- * primitives directly from `@cartbase/storefront/primitives/ui/select`.
15
- *
16
- * Extracted from mindpages-storefront
17
- * src/modules/checkout/templates/checkout-client/index.tsx lines 207-270.
18
- *
19
- * @example
20
- * <SelectField label="Country" name="country" value={country} onChange={...}>
21
- * <option value="bg">Bulgaria</option>
22
- * <option value="de">Germany</option>
23
- * </SelectField>
24
- */
25
-
26
- export type SelectFieldProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
27
- label: string
28
- children: React.ReactNode
29
- className?: string
30
- /**
31
- * A mark shown inside the control, before the value: a flag, a currency
32
- * symbol. Decorative and click-through, so the whole field still opens the
33
- * select. The value and the floating label make room for it.
34
- */
35
- leading?: React.ReactNode
36
- }
37
-
38
- export const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(
39
- function SelectField(
40
- { label, required, disabled, children, className, value, leading, ...props },
41
- ref
42
- ) {
43
- const hasValue = value !== undefined && value !== null && value !== ""
44
-
45
- return (
46
- <div className={cn("relative", className)}>
47
- <select
48
- ref={ref}
49
- required={required}
50
- disabled={disabled}
51
- value={value}
52
- className={cn(
53
- "w-full h-[44px] pt-[14px] pb-[2px] pr-9 text-sm rounded-lg border transition-colors duration-150 appearance-none outline-none",
54
- leading ? "pl-10" : "px-3",
55
- "focus:border-primary focus:bg-primary/5",
56
- disabled
57
- ? "bg-muted border-border text-muted-foreground cursor-not-allowed"
58
- : "bg-card border-border text-foreground hover:border-muted-foreground"
59
- )}
60
- {...props}
61
- >
62
- {children}
63
- </select>
64
- {leading && (
65
- <span className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none flex items-center">
66
- {leading}
67
- </span>
68
- )}
69
- <span
70
- className={cn(
71
- "absolute pointer-events-none top-[14px] text-sm leading-4 origin-top-left",
72
- leading ? "left-10" : "left-3",
73
- "transition-transform transition-colors duration-150 ease-out",
74
- hasValue ? "text-muted-foreground" : "text-muted-foreground",
75
- hasValue ? "-translate-y-2 scale-[0.77]" : "translate-y-0 scale-100"
76
- )}
77
- >
78
- {label}
79
- </span>
80
- <svg
81
- className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none"
82
- fill="none"
83
- viewBox="0 0 24 24"
84
- stroke="currentColor"
85
- strokeWidth={1.5}
86
- aria-hidden="true"
87
- >
88
- <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
89
- </svg>
90
- </div>
91
- )
92
- }
93
- )
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ /**
8
+ * SelectField — floating-label native select primitive.
9
+ *
10
+ * Matches `Field`'s visual treatment — the label floats into the top-left
11
+ * when a value is selected. Uses a plain native `<select>` for simplicity
12
+ * and native mobile keyboard support. If a fancier dropdown is needed
13
+ * (searchable, multi-select, rich options), use the shadcn `Select`
14
+ * primitives directly from `@cartbase/storefront/primitives/ui/select`.
15
+ *
16
+ * Extracted from mindpages-storefront
17
+ * src/modules/checkout/templates/checkout-client/index.tsx lines 207-270.
18
+ *
19
+ * @example
20
+ * <SelectField label="Country" name="country" value={country} onChange={...}>
21
+ * <option value="bg">Bulgaria</option>
22
+ * <option value="de">Germany</option>
23
+ * </SelectField>
24
+ */
25
+
26
+ export type SelectFieldProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
27
+ label: string
28
+ children: React.ReactNode
29
+ className?: string
30
+ /**
31
+ * A mark shown inside the control, before the value: a flag, a currency
32
+ * symbol. Decorative and click-through, so the whole field still opens the
33
+ * select. The value and the floating label make room for it.
34
+ */
35
+ leading?: React.ReactNode
36
+ }
37
+
38
+ export const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(
39
+ function SelectField(
40
+ { label, required, disabled, children, className, value, leading, ...props },
41
+ ref
42
+ ) {
43
+ const hasValue = value !== undefined && value !== null && value !== ""
44
+
45
+ return (
46
+ <div className={cn("relative", className)}>
47
+ <select
48
+ ref={ref}
49
+ required={required}
50
+ disabled={disabled}
51
+ value={value}
52
+ className={cn(
53
+ "w-full h-[44px] pt-[14px] pb-[2px] pr-9 text-sm rounded-lg border transition-colors duration-150 appearance-none outline-none",
54
+ leading ? "pl-10" : "px-3",
55
+ "focus:border-primary focus:bg-primary/5",
56
+ disabled
57
+ ? "bg-muted border-border text-muted-foreground cursor-not-allowed"
58
+ : "bg-card border-border text-foreground hover:border-muted-foreground"
59
+ )}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </select>
64
+ {leading && (
65
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none flex items-center">
66
+ {leading}
67
+ </span>
68
+ )}
69
+ <span
70
+ className={cn(
71
+ "absolute pointer-events-none top-[14px] text-sm leading-4 origin-top-left",
72
+ leading ? "left-10" : "left-3",
73
+ "transition-transform transition-colors duration-150 ease-out",
74
+ hasValue ? "text-muted-foreground" : "text-muted-foreground",
75
+ hasValue ? "-translate-y-2 scale-[0.77]" : "translate-y-0 scale-100"
76
+ )}
77
+ >
78
+ {label}
79
+ </span>
80
+ <svg
81
+ className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none"
82
+ fill="none"
83
+ viewBox="0 0 24 24"
84
+ stroke="currentColor"
85
+ strokeWidth={1.5}
86
+ aria-hidden="true"
87
+ >
88
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
89
+ </svg>
90
+ </div>
91
+ )
92
+ }
93
+ )
@@ -1,43 +1,43 @@
1
- /**
2
- * PDP image gallery — ported from
3
- * `@1click/ui/src/products/image-gallery.tsx` (v2.3.1). Data seam:
4
- * `HttpTypes.StoreProductImage[]` → Cartbase `StoreProductImage[]`
5
- * (`@cartbase/storefront/api/products`; images arrive rank-ordered from the
6
- * API). Server-safe.
7
- */
8
- import { StoreImage as Image } from "../lib/media-image"
9
- import type { StoreProductImage } from "../api/products"
10
-
11
- type ImageGalleryProps = {
12
- images: StoreProductImage[]
13
- }
14
-
15
- const ImageGallery = ({ images }: ImageGalleryProps) => {
16
- return (
17
- <div className="flex items-start relative">
18
- <div className="flex flex-col flex-1 sm:mx-16 gap-y-4">
19
- {images.map((image, index) => (
20
- <div
21
- key={image.id}
22
- className="relative aspect-[29/34] w-full overflow-hidden bg-muted rounded-lg"
23
- id={image.id}
24
- >
25
- {!!image.url && (
26
- <Image
27
- src={image.url}
28
- priority={index <= 2}
29
- className="absolute inset-0 rounded-lg"
30
- alt={`Product image ${index + 1}`}
31
- fill
32
- sizes="(max-width: 576px) 280px, (max-width: 768px) 360px, (max-width: 992px) 480px, 800px"
33
- style={{ objectFit: "cover" }}
34
- />
35
- )}
36
- </div>
37
- ))}
38
- </div>
39
- </div>
40
- )
41
- }
42
-
43
- export { ImageGallery, type ImageGalleryProps }
1
+ /**
2
+ * PDP image gallery — ported from
3
+ * `@1click/ui/src/products/image-gallery.tsx` (v2.3.1). Data seam:
4
+ * `HttpTypes.StoreProductImage[]` → Cartbase `StoreProductImage[]`
5
+ * (`@cartbase/storefront/api/products`; images arrive rank-ordered from the
6
+ * API). Server-safe.
7
+ */
8
+ import { StoreImage as Image } from "../lib/media-image"
9
+ import type { StoreProductImage } from "../api/products"
10
+
11
+ type ImageGalleryProps = {
12
+ images: StoreProductImage[]
13
+ }
14
+
15
+ const ImageGallery = ({ images }: ImageGalleryProps) => {
16
+ return (
17
+ <div className="flex items-start relative">
18
+ <div className="flex flex-col flex-1 sm:mx-16 gap-y-4">
19
+ {images.map((image, index) => (
20
+ <div
21
+ key={image.id}
22
+ className="relative aspect-[29/34] w-full overflow-hidden bg-muted rounded-lg"
23
+ id={image.id}
24
+ >
25
+ {!!image.url && (
26
+ <Image
27
+ src={image.url}
28
+ priority={index <= 2}
29
+ className="absolute inset-0 rounded-lg"
30
+ alt={`Product image ${index + 1}`}
31
+ fill
32
+ sizes="(max-width: 576px) 280px, (max-width: 768px) 360px, (max-width: 992px) 480px, 800px"
33
+ style={{ objectFit: "cover" }}
34
+ />
35
+ )}
36
+ </div>
37
+ ))}
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export { ImageGallery, type ImageGalleryProps }
@@ -1,67 +1,67 @@
1
- "use client"
2
-
3
- /**
4
- * Option selector — ported from
5
- * `@1click/ui/src/products/option-select.tsx` (v2.3.1). Data seam:
6
- * `HttpTypes.StoreProductOption` → Cartbase `StoreProductOption`
7
- * (`@cartbase/storefront/api/products`; `values` is present on
8
- * `product.options[]`, which is what this component receives).
9
- */
10
- import React from "react"
11
- import type { StoreProductOption } from "../api/products"
12
- import { cn } from "../lib/utils"
13
- import { useProductLabels } from "./context"
14
-
15
- type OptionSelectProps = {
16
- option: StoreProductOption
17
- current: string | undefined
18
- updateOption: (optionId: string, value: string) => void
19
- title: string
20
- disabled: boolean
21
- "data-testid"?: string
22
- }
23
-
24
- const OptionSelect: React.FC<OptionSelectProps> = ({
25
- option,
26
- current,
27
- updateOption,
28
- title,
29
- "data-testid": dataTestId,
30
- disabled,
31
- }) => {
32
- const labels = useProductLabels()
33
- const filteredOptions = (option.values ?? []).map((v) => v.value)
34
-
35
- return (
36
- <div className="flex flex-col gap-y-3">
37
- <span className="text-sm text-foreground">
38
- {labels.selectOption} {title}
39
- </span>
40
- <div
41
- className="flex flex-wrap justify-between gap-2"
42
- data-testid={dataTestId}
43
- >
44
- {filteredOptions.map((v) => (
45
- <button
46
- onClick={() => updateOption(option.id, v!)}
47
- key={v}
48
- className={cn(
49
- "border border-border bg-muted text-sm h-10 rounded-lg p-2 flex-1",
50
- {
51
- "border-primary": v === current,
52
- "hover:shadow-sm transition-shadow ease-in-out duration-150":
53
- v !== current,
54
- }
55
- )}
56
- disabled={disabled}
57
- data-testid="option-button"
58
- >
59
- {v}
60
- </button>
61
- ))}
62
- </div>
63
- </div>
64
- )
65
- }
66
-
67
- export { OptionSelect, type OptionSelectProps }
1
+ "use client"
2
+
3
+ /**
4
+ * Option selector — ported from
5
+ * `@1click/ui/src/products/option-select.tsx` (v2.3.1). Data seam:
6
+ * `HttpTypes.StoreProductOption` → Cartbase `StoreProductOption`
7
+ * (`@cartbase/storefront/api/products`; `values` is present on
8
+ * `product.options[]`, which is what this component receives).
9
+ */
10
+ import React from "react"
11
+ import type { StoreProductOption } from "../api/products"
12
+ import { cn } from "../lib/utils"
13
+ import { useProductLabels } from "./context"
14
+
15
+ type OptionSelectProps = {
16
+ option: StoreProductOption
17
+ current: string | undefined
18
+ updateOption: (optionId: string, value: string) => void
19
+ title: string
20
+ disabled: boolean
21
+ "data-testid"?: string
22
+ }
23
+
24
+ const OptionSelect: React.FC<OptionSelectProps> = ({
25
+ option,
26
+ current,
27
+ updateOption,
28
+ title,
29
+ "data-testid": dataTestId,
30
+ disabled,
31
+ }) => {
32
+ const labels = useProductLabels()
33
+ const filteredOptions = (option.values ?? []).map((v) => v.value)
34
+
35
+ return (
36
+ <div className="flex flex-col gap-y-3">
37
+ <span className="text-sm text-foreground">
38
+ {labels.selectOption} {title}
39
+ </span>
40
+ <div
41
+ className="flex flex-wrap justify-between gap-2"
42
+ data-testid={dataTestId}
43
+ >
44
+ {filteredOptions.map((v) => (
45
+ <button
46
+ onClick={() => updateOption(option.id, v!)}
47
+ key={v}
48
+ className={cn(
49
+ "border border-border bg-muted text-sm h-10 rounded-lg p-2 flex-1",
50
+ {
51
+ "border-primary": v === current,
52
+ "hover:shadow-sm transition-shadow ease-in-out duration-150":
53
+ v !== current,
54
+ }
55
+ )}
56
+ disabled={disabled}
57
+ data-testid="option-button"
58
+ >
59
+ {v}
60
+ </button>
61
+ ))}
62
+ </div>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ export { OptionSelect, type OptionSelectProps }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * PDP title/collection/description block — ported from
3
- * `@1click/ui/src/products/product-info.tsx` (v2.3.1). Data seam:
4
- * `HttpTypes.StoreProduct` Cartbase `StoreProduct`. The product carries
5
- * EVERY collection it belongs to since 2026-09-15 (the membership join is
6
- * the one store); the line above the title names the first of them, which is
7
- * what the single `collection` object used to be. Server-safe.
3
+ * `@1click/ui/src/products/product-info.tsx` (v2.3.1). Data seam: Cartbase's
4
+ * own `StoreProduct` stands in for `HttpTypes.StoreProduct`. The product
5
+ * carries EVERY collection it belongs to since 2026-09-15 (the membership
6
+ * join is the one store); the line above the title names the first of them,
7
+ * which is what the single `collection` object used to be. Server-safe.
8
8
  */
9
9
  import Link from "next/link"
10
10
  import type { StoreProduct } from "../api/products"
@@ -1,69 +1,69 @@
1
- "use client"
2
-
3
- /**
4
- * PDP price display — ported from
5
- * `@1click/ui/src/products/product-price.tsx` (v2.3.1). Data seam:
6
- * `@medusajs/types` `HttpTypes.StoreProduct/StoreProductVariant` are
7
- * replaced by the Cartbase SDK DTOs (`@cartbase/storefront/api/products`).
8
- * Pricing stays SERVER truth — `lib/get-product-price` only selects and
9
- * formats `variant.calculated_price` (Cartbase's flat `price_list_type`).
10
- */
11
- import type { StoreProduct, StoreProductVariant } from "../api/products"
12
- import { cn } from "../lib/utils"
13
- import { getProductPrice } from "../lib/get-product-price"
14
- import { useProductLabels } from "./context"
15
-
16
- export function ProductPrice({
17
- product,
18
- variant,
19
- }: {
20
- product: StoreProduct
21
- variant?: StoreProductVariant
22
- }) {
23
- const labels = useProductLabels()
24
- const { cheapestPrice, variantPrice } = getProductPrice({
25
- product,
26
- variantId: variant?.id,
27
- })
28
-
29
- const selectedPrice = variant ? variantPrice : cheapestPrice
30
-
31
- if (!selectedPrice) {
32
- return <div className="block w-32 h-9 bg-muted animate-pulse rounded" />
33
- }
34
-
35
- return (
36
- <div className="flex flex-col text-foreground">
37
- <span
38
- className={cn("text-xl font-semibold", {
39
- "text-primary": selectedPrice.price_type === "sale",
40
- })}
41
- >
42
- {!variant && `${labels.from} `}
43
- <span
44
- data-testid="product-price"
45
- data-value={selectedPrice.calculated_price_number}
46
- >
47
- {selectedPrice.calculated_price}
48
- </span>
49
- </span>
50
- {selectedPrice.price_type === "sale" && (
51
- <>
52
- <p>
53
- <span className="text-muted-foreground">{labels.original} </span>
54
- <span
55
- className="line-through"
56
- data-testid="original-product-price"
57
- data-value={selectedPrice.original_price_number}
58
- >
59
- {selectedPrice.original_price}
60
- </span>
61
- </p>
62
- <span className="text-primary">
63
- -{selectedPrice.percentage_diff}%
64
- </span>
65
- </>
66
- )}
67
- </div>
68
- )
69
- }
1
+ "use client"
2
+
3
+ /**
4
+ * PDP price display — ported from
5
+ * `@1click/ui/src/products/product-price.tsx` (v2.3.1). Data seam:
6
+ * `@medusajs/types` `HttpTypes.StoreProduct/StoreProductVariant` are
7
+ * replaced by the Cartbase SDK DTOs (`@cartbase/storefront/api/products`).
8
+ * Pricing stays SERVER truth — `lib/get-product-price` only selects and
9
+ * formats `variant.calculated_price` (Cartbase's flat `price_list_type`).
10
+ */
11
+ import type { StoreProduct, StoreProductVariant } from "../api/products"
12
+ import { cn } from "../lib/utils"
13
+ import { getProductPrice } from "../lib/get-product-price"
14
+ import { useProductLabels } from "./context"
15
+
16
+ export function ProductPrice({
17
+ product,
18
+ variant,
19
+ }: {
20
+ product: StoreProduct
21
+ variant?: StoreProductVariant
22
+ }) {
23
+ const labels = useProductLabels()
24
+ const { cheapestPrice, variantPrice } = getProductPrice({
25
+ product,
26
+ variantId: variant?.id,
27
+ })
28
+
29
+ const selectedPrice = variant ? variantPrice : cheapestPrice
30
+
31
+ if (!selectedPrice) {
32
+ return <div className="block w-32 h-9 bg-muted animate-pulse rounded" />
33
+ }
34
+
35
+ return (
36
+ <div className="flex flex-col text-foreground">
37
+ <span
38
+ className={cn("text-xl font-semibold", {
39
+ "text-primary": selectedPrice.price_type === "sale",
40
+ })}
41
+ >
42
+ {!variant && `${labels.from} `}
43
+ <span
44
+ data-testid="product-price"
45
+ data-value={selectedPrice.calculated_price_number}
46
+ >
47
+ {selectedPrice.calculated_price}
48
+ </span>
49
+ </span>
50
+ {selectedPrice.price_type === "sale" && (
51
+ <>
52
+ <p>
53
+ <span className="text-muted-foreground">{labels.original} </span>
54
+ <span
55
+ className="line-through"
56
+ data-testid="original-product-price"
57
+ data-value={selectedPrice.original_price_number}
58
+ >
59
+ {selectedPrice.original_price}
60
+ </span>
61
+ </p>
62
+ <span className="text-primary">
63
+ -{selectedPrice.percentage_diff}%
64
+ </span>
65
+ </>
66
+ )}
67
+ </div>
68
+ )
69
+ }