@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,250 +1,250 @@
1
- import { Price } from "../lib/price"
2
- import { findFeeLine, type LineLike } from "../lib/cart-helpers"
3
- import { defaultOrderLabels, type OrderLabels } from "./labels"
4
-
5
- /**
6
- * Order totals breakdown. Ported from `@1click/ui/src/order/order-totals.tsx`
7
- * (v2.3.1) with the Cartbase money seam:
8
- *
9
- * Totals are SERVER truth and the Cartbase store order read
10
- * (`StoreOrderDetail`) carries NONE of them — they live where the server
11
- * computed them: the decorated cart the app holds on the confirmation page
12
- * (`api/carts` `Cart`: `item_subtotal`, `shipping_subtotal`,
13
- * `discount_total`, `tax_total`, `total`, `payment_method_fee_total`, `payment_method_fee_label`)
14
- * or the order's summary snapshot (`order_summaries.totals` — reachable via
15
- * `completeCart().order.summary`). The component takes a structural
16
- * `OrderTotalsSource` both of those satisfy (`orderTotalsFromSummary()`
17
- * adapts the summary shape) and only SELECTS + FORMATS rows — it never
18
- * computes money beyond the two production subtractions documented below.
19
- *
20
- * Method-fee display, in preference order (production fix preserved):
21
- * 1. Cartbase-native fee: `payment_method_fee_total` > 0 (never a line
22
- * item; already folded into `total` server-side), labeled by the
23
- * merchant-editable `payment_method_fee_label`. Pre-rename summaries
24
- * (2026-08-11) spelled the keys cod_fee_* — read as legacy fallback.
25
- * 2. legacy Medusa-era fee LINE ITEM (`metadata.is_cod_fee`) found in
26
- * `items` — its net is subtracted from the visible subtotal so the
27
- * Subtotal row stays product-only while Tax keeps the fee's tax
28
- * portion (v2.3.1 fix).
29
- * 3. label fallbacks: `methodFeeLabel` prop → source label →
30
- * fee line's own title → translated `labels.paymentMethodFee`.
31
- */
32
-
33
- /** Structural totals source — `api/carts` `Cart` satisfies it directly. */
34
- export interface OrderTotalsSource {
35
- /** Product-only net subtotal (cart path). Preferred. */
36
- item_subtotal?: number | null
37
- /** Combined items+shipping net (summary path fallback). */
38
- subtotal?: number | null
39
- /** Shipping net (cart path). Preferred for the Shipping row. */
40
- shipping_subtotal?: number | null
41
- /** Shipping gross (summary path fallback). */
42
- shipping_total?: number | null
43
- discount_total?: number | null
44
- tax_total?: number | null
45
- total?: number | null
46
- /** Cartbase-native method fee (already folded into `total`). */
47
- payment_method_fee_total?: number | null
48
- /** The method's fee label from the server. */
49
- payment_method_fee_label?: string | null
50
- /** LEGACY pre-rename summary keys (2026-08-11) — read-only fallback. */
51
- cod_fee_total?: number | null
52
- cod_fee_label?: string | null
53
- [key: string]: unknown
54
- }
55
-
56
- /**
57
- * Adapt `order_summaries.totals` (shape:
58
- * `{subtotal, total, tax_total, discount_total, shipping_total,
59
- * payment_method_fee_total}` — src/app/api/store/carts/[id]/complete/_copy.ts) to an
60
- * `OrderTotalsSource`. Accepts the raw `CompletedOrder["summary"]` value
61
- * (embed row, array of rows, or the totals object itself); null when no
62
- * totals can be found.
63
- */
64
- export function orderTotalsFromSummary(
65
- summary: unknown
66
- ): OrderTotalsSource | null {
67
- if (!summary || typeof summary !== "object") return null
68
- const first = Array.isArray(summary) ? summary[0] : summary
69
- if (!first || typeof first !== "object") return null
70
- const rec = first as Record<string, unknown>
71
- const totals =
72
- rec.totals && typeof rec.totals === "object"
73
- ? (rec.totals as Record<string, unknown>)
74
- : rec
75
- if (typeof totals.total !== "number") return null
76
- return totals as OrderTotalsSource
77
- }
78
-
79
- export type OrderTotalsRow = {
80
- key: "subtotal" | "shipping" | "methodFee" | "discount" | "tax"
81
- /** Resolved display label (methodFee row only — others use the label pack). */
82
- label?: string
83
- amount: number
84
- /** Shipping row at 0 renders the FREE badge instead of a price. */
85
- free?: boolean
86
- /** Discount row renders negated. */
87
- negative?: boolean
88
- }
89
-
90
- /**
91
- * Pure row selection — the entire display policy of the breakdown, kept
92
- * out of JSX so it is unit-testable (tests/unit/storefront-order-reviews).
93
- */
94
- export function selectOrderTotalsRows(
95
- source: OrderTotalsSource,
96
- opts: {
97
- items?: readonly LineLike[] | null
98
- methodFeeLabel?: string
99
- labels?: Pick<OrderLabels, "paymentMethodFee">
100
- } = {}
101
- ): { rows: OrderTotalsRow[]; total: number } {
102
- const l = { ...defaultOrderLabels, ...opts.labels }
103
- const rows: OrderTotalsRow[] = []
104
-
105
- // Legacy fee LINE (Medusa-era injection) — Cartbase-native carts never
106
- // have one, but ported data / hybrid stores might.
107
- const feeItem = findFeeLine(opts.items || [])
108
- const nativeFee = Number(source.payment_method_fee_total ?? source.cod_fee_total ?? 0)
109
- const lineFee = Number(feeItem?.total ?? 0)
110
- const methodFeeAmount = nativeFee > 0 ? nativeFee : lineFee
111
-
112
- // Subtotal: prefer the product-only net; fall back to combined-minus-
113
- // shipping, then combined. Subtract a legacy fee line's net so the
114
- // visible Subtotal stays product-only (v2.3.1 production fix).
115
- const baseSubtotal =
116
- source.item_subtotal ??
117
- (source.subtotal !== null &&
118
- source.subtotal !== undefined &&
119
- source.shipping_subtotal !== null &&
120
- source.shipping_subtotal !== undefined
121
- ? Number(source.subtotal) - Number(source.shipping_subtotal)
122
- : source.subtotal ?? 0)
123
- const feeNet = feeItem
124
- ? Number(feeItem.subtotal ?? feeItem.total ?? 0)
125
- : 0
126
- rows.push({
127
- key: "subtotal",
128
- amount: Math.max(0, Number(baseSubtotal ?? 0) - feeNet),
129
- })
130
-
131
- const shippingAmount = Number(
132
- source.shipping_subtotal ?? source.shipping_total ?? 0
133
- )
134
- rows.push({
135
- key: "shipping",
136
- amount: shippingAmount,
137
- free: shippingAmount === 0,
138
- })
139
-
140
- if (methodFeeAmount > 0) {
141
- rows.push({
142
- key: "methodFee",
143
- amount: methodFeeAmount,
144
- label:
145
- opts.methodFeeLabel ??
146
- source.payment_method_fee_label ??
147
- source.cod_fee_label ??
148
- ((feeItem as { title?: string } | null)?.title || undefined) ??
149
- l.paymentMethodFee,
150
- })
151
- }
152
-
153
- const discount = Number(source.discount_total ?? 0)
154
- if (discount > 0) {
155
- rows.push({ key: "discount", amount: discount, negative: true })
156
- }
157
-
158
- rows.push({ key: "tax", amount: Number(source.tax_total ?? 0) })
159
-
160
- return { rows, total: Number(source.total ?? 0) }
161
- }
162
-
163
- type OrderTotalsProps = {
164
- totals: OrderTotalsSource
165
- currencyCode: string
166
- /** Lines from the same source — used only to detect a legacy fee line. */
167
- items?: readonly LineLike[] | null
168
- labels?: Pick<
169
- OrderLabels,
170
- "subtotal" | "shipping" | "discount" | "tax" | "total" | "free" | "paymentMethodFee"
171
- >
172
- /**
173
- * Per-store override for the method-fee row label. Optional — falls back
174
- * to the server's `payment_method_fee_label`, then the fee line's title,
175
- * then the translated `labels.paymentMethodFee` default.
176
- */
177
- methodFeeLabel?: string
178
- }
179
-
180
- export function OrderTotals({
181
- totals,
182
- currencyCode,
183
- items,
184
- labels,
185
- methodFeeLabel,
186
- }: OrderTotalsProps) {
187
- const l = { ...defaultOrderLabels, ...labels }
188
- const { rows, total } = selectOrderTotalsRows(totals, {
189
- items,
190
- methodFeeLabel,
191
- labels: l,
192
- })
193
-
194
- const labelFor = (row: OrderTotalsRow): string => {
195
- switch (row.key) {
196
- case "subtotal":
197
- return l.subtotal
198
- case "shipping":
199
- return l.shipping
200
- case "methodFee":
201
- return row.label ?? l.paymentMethodFee
202
- case "discount":
203
- return l.discount
204
- case "tax":
205
- return l.tax
206
- }
207
- }
208
-
209
- return (
210
- <div className="pt-4 border-t border-border">
211
- <div className="flex flex-col gap-2 text-sm">
212
- {rows.map((row) => (
213
- <div key={row.key} className="flex justify-between">
214
- <span className="text-muted-foreground">{labelFor(row)}</span>
215
- {row.free ? (
216
- <span className="text-sm font-medium text-success">{l.free}</span>
217
- ) : row.negative ? (
218
- <span className="text-sm text-success">
219
- -{" "}
220
- <Price
221
- amount={row.amount}
222
- currencyCode={currencyCode}
223
- className="text-sm text-success"
224
- />
225
- </span>
226
- ) : (
227
- <Price
228
- amount={row.amount}
229
- currencyCode={currencyCode}
230
- className="text-sm text-foreground"
231
- />
232
- )}
233
- </div>
234
- ))}
235
- </div>
236
-
237
- <div className="h-px bg-border my-3" />
238
- <div className="flex justify-between items-baseline">
239
- <span className="text-[15px] font-bold text-foreground">{l.total}</span>
240
- <Price
241
- amount={total}
242
- currencyCode={currencyCode}
243
- className="text-xl font-bold text-foreground tracking-tight"
244
- />
245
- </div>
246
- </div>
247
- )
248
- }
249
-
250
- export { type OrderTotalsProps }
1
+ import { Price } from "../lib/price"
2
+ import { findFeeLine, type LineLike } from "../lib/cart-helpers"
3
+ import { defaultOrderLabels, type OrderLabels } from "./labels"
4
+
5
+ /**
6
+ * Order totals breakdown. Ported from `@1click/ui/src/order/order-totals.tsx`
7
+ * (v2.3.1) with the Cartbase money seam:
8
+ *
9
+ * Totals are SERVER truth and the Cartbase store order read
10
+ * (`StoreOrderDetail`) carries NONE of them — they live where the server
11
+ * computed them: the decorated cart the app holds on the confirmation page
12
+ * (`api/carts` `Cart`: `item_subtotal`, `shipping_subtotal`,
13
+ * `discount_total`, `tax_total`, `total`, `payment_method_fee_total`, `payment_method_fee_label`)
14
+ * or the order's summary snapshot (`order_summaries.totals` — reachable via
15
+ * `completeCart().order.summary`). The component takes a structural
16
+ * `OrderTotalsSource` both of those satisfy (`orderTotalsFromSummary()`
17
+ * adapts the summary shape) and only SELECTS + FORMATS rows — it never
18
+ * computes money beyond the two production subtractions documented below.
19
+ *
20
+ * Method-fee display, in preference order (production fix preserved):
21
+ * 1. Cartbase-native fee: `payment_method_fee_total` > 0 (never a line
22
+ * item; already folded into `total` server-side), labeled by the
23
+ * merchant-editable `payment_method_fee_label`. Pre-rename summaries
24
+ * (2026-08-11) spelled the keys cod_fee_* — read as legacy fallback.
25
+ * 2. legacy Medusa-era fee LINE ITEM (`metadata.is_cod_fee`) found in
26
+ * `items` — its net is subtracted from the visible subtotal so the
27
+ * Subtotal row stays product-only while Tax keeps the fee's tax
28
+ * portion (v2.3.1 fix).
29
+ * 3. label fallbacks: `methodFeeLabel` prop → source label →
30
+ * fee line's own title → translated `labels.paymentMethodFee`.
31
+ */
32
+
33
+ /** Structural totals source — `api/carts` `Cart` satisfies it directly. */
34
+ export interface OrderTotalsSource {
35
+ /** Product-only net subtotal (cart path). Preferred. */
36
+ item_subtotal?: number | null
37
+ /** Combined items+shipping net (summary path fallback). */
38
+ subtotal?: number | null
39
+ /** Shipping net (cart path). Preferred for the Shipping row. */
40
+ shipping_subtotal?: number | null
41
+ /** Shipping gross (summary path fallback). */
42
+ shipping_total?: number | null
43
+ discount_total?: number | null
44
+ tax_total?: number | null
45
+ total?: number | null
46
+ /** Cartbase-native method fee (already folded into `total`). */
47
+ payment_method_fee_total?: number | null
48
+ /** The method's fee label from the server. */
49
+ payment_method_fee_label?: string | null
50
+ /** LEGACY pre-rename summary keys (2026-08-11) — read-only fallback. */
51
+ cod_fee_total?: number | null
52
+ cod_fee_label?: string | null
53
+ [key: string]: unknown
54
+ }
55
+
56
+ /**
57
+ * Adapt `order_summaries.totals` (shape:
58
+ * `{subtotal, total, tax_total, discount_total, shipping_total,
59
+ * payment_method_fee_total}` — src/app/api/store/carts/[id]/complete/_copy.ts) to an
60
+ * `OrderTotalsSource`. Accepts the raw `CompletedOrder["summary"]` value
61
+ * (embed row, array of rows, or the totals object itself); null when no
62
+ * totals can be found.
63
+ */
64
+ export function orderTotalsFromSummary(
65
+ summary: unknown
66
+ ): OrderTotalsSource | null {
67
+ if (!summary || typeof summary !== "object") return null
68
+ const first = Array.isArray(summary) ? summary[0] : summary
69
+ if (!first || typeof first !== "object") return null
70
+ const rec = first as Record<string, unknown>
71
+ const totals =
72
+ rec.totals && typeof rec.totals === "object"
73
+ ? (rec.totals as Record<string, unknown>)
74
+ : rec
75
+ if (typeof totals.total !== "number") return null
76
+ return totals as OrderTotalsSource
77
+ }
78
+
79
+ export type OrderTotalsRow = {
80
+ key: "subtotal" | "shipping" | "methodFee" | "discount" | "tax"
81
+ /** Resolved display label (methodFee row only — others use the label pack). */
82
+ label?: string
83
+ amount: number
84
+ /** Shipping row at 0 renders the FREE badge instead of a price. */
85
+ free?: boolean
86
+ /** Discount row renders negated. */
87
+ negative?: boolean
88
+ }
89
+
90
+ /**
91
+ * Pure row selection — the entire display policy of the breakdown, kept
92
+ * out of JSX so it is unit-testable (tests/unit/storefront-order-reviews).
93
+ */
94
+ export function selectOrderTotalsRows(
95
+ source: OrderTotalsSource,
96
+ opts: {
97
+ items?: readonly LineLike[] | null
98
+ methodFeeLabel?: string
99
+ labels?: Pick<OrderLabels, "paymentMethodFee">
100
+ } = {}
101
+ ): { rows: OrderTotalsRow[]; total: number } {
102
+ const l = { ...defaultOrderLabels, ...opts.labels }
103
+ const rows: OrderTotalsRow[] = []
104
+
105
+ // Legacy fee LINE (Medusa-era injection) — Cartbase-native carts never
106
+ // have one, but ported data / hybrid stores might.
107
+ const feeItem = findFeeLine(opts.items || [])
108
+ const nativeFee = Number(source.payment_method_fee_total ?? source.cod_fee_total ?? 0)
109
+ const lineFee = Number(feeItem?.total ?? 0)
110
+ const methodFeeAmount = nativeFee > 0 ? nativeFee : lineFee
111
+
112
+ // Subtotal: prefer the product-only net; fall back to combined-minus-
113
+ // shipping, then combined. Subtract a legacy fee line's net so the
114
+ // visible Subtotal stays product-only (v2.3.1 production fix).
115
+ const baseSubtotal =
116
+ source.item_subtotal ??
117
+ (source.subtotal !== null &&
118
+ source.subtotal !== undefined &&
119
+ source.shipping_subtotal !== null &&
120
+ source.shipping_subtotal !== undefined
121
+ ? Number(source.subtotal) - Number(source.shipping_subtotal)
122
+ : source.subtotal ?? 0)
123
+ const feeNet = feeItem
124
+ ? Number(feeItem.subtotal ?? feeItem.total ?? 0)
125
+ : 0
126
+ rows.push({
127
+ key: "subtotal",
128
+ amount: Math.max(0, Number(baseSubtotal ?? 0) - feeNet),
129
+ })
130
+
131
+ const shippingAmount = Number(
132
+ source.shipping_subtotal ?? source.shipping_total ?? 0
133
+ )
134
+ rows.push({
135
+ key: "shipping",
136
+ amount: shippingAmount,
137
+ free: shippingAmount === 0,
138
+ })
139
+
140
+ if (methodFeeAmount > 0) {
141
+ rows.push({
142
+ key: "methodFee",
143
+ amount: methodFeeAmount,
144
+ label:
145
+ opts.methodFeeLabel ??
146
+ source.payment_method_fee_label ??
147
+ source.cod_fee_label ??
148
+ ((feeItem as { title?: string } | null)?.title || undefined) ??
149
+ l.paymentMethodFee,
150
+ })
151
+ }
152
+
153
+ const discount = Number(source.discount_total ?? 0)
154
+ if (discount > 0) {
155
+ rows.push({ key: "discount", amount: discount, negative: true })
156
+ }
157
+
158
+ rows.push({ key: "tax", amount: Number(source.tax_total ?? 0) })
159
+
160
+ return { rows, total: Number(source.total ?? 0) }
161
+ }
162
+
163
+ type OrderTotalsProps = {
164
+ totals: OrderTotalsSource
165
+ currencyCode: string
166
+ /** Lines from the same source — used only to detect a legacy fee line. */
167
+ items?: readonly LineLike[] | null
168
+ labels?: Pick<
169
+ OrderLabels,
170
+ "subtotal" | "shipping" | "discount" | "tax" | "total" | "free" | "paymentMethodFee"
171
+ >
172
+ /**
173
+ * Per-store override for the method-fee row label. Optional — falls back
174
+ * to the server's `payment_method_fee_label`, then the fee line's title,
175
+ * then the translated `labels.paymentMethodFee` default.
176
+ */
177
+ methodFeeLabel?: string
178
+ }
179
+
180
+ export function OrderTotals({
181
+ totals,
182
+ currencyCode,
183
+ items,
184
+ labels,
185
+ methodFeeLabel,
186
+ }: OrderTotalsProps) {
187
+ const l = { ...defaultOrderLabels, ...labels }
188
+ const { rows, total } = selectOrderTotalsRows(totals, {
189
+ items,
190
+ methodFeeLabel,
191
+ labels: l,
192
+ })
193
+
194
+ const labelFor = (row: OrderTotalsRow): string => {
195
+ switch (row.key) {
196
+ case "subtotal":
197
+ return l.subtotal
198
+ case "shipping":
199
+ return l.shipping
200
+ case "methodFee":
201
+ return row.label ?? l.paymentMethodFee
202
+ case "discount":
203
+ return l.discount
204
+ case "tax":
205
+ return l.tax
206
+ }
207
+ }
208
+
209
+ return (
210
+ <div className="pt-4 border-t border-border">
211
+ <div className="flex flex-col gap-2 text-sm">
212
+ {rows.map((row) => (
213
+ <div key={row.key} className="flex justify-between">
214
+ <span className="text-muted-foreground">{labelFor(row)}</span>
215
+ {row.free ? (
216
+ <span className="text-sm font-medium text-success">{l.free}</span>
217
+ ) : row.negative ? (
218
+ <span className="text-sm text-success">
219
+ -{" "}
220
+ <Price
221
+ amount={row.amount}
222
+ currencyCode={currencyCode}
223
+ className="text-sm text-success"
224
+ />
225
+ </span>
226
+ ) : (
227
+ <Price
228
+ amount={row.amount}
229
+ currencyCode={currencyCode}
230
+ className="text-sm text-foreground"
231
+ />
232
+ )}
233
+ </div>
234
+ ))}
235
+ </div>
236
+
237
+ <div className="h-px bg-border my-3" />
238
+ <div className="flex justify-between items-baseline">
239
+ <span className="text-[15px] font-bold text-foreground">{l.total}</span>
240
+ <Price
241
+ amount={total}
242
+ currencyCode={currencyCode}
243
+ className="text-xl font-bold text-foreground tracking-tight"
244
+ />
245
+ </div>
246
+ </div>
247
+ )
248
+ }
249
+
250
+ export { type OrderTotalsProps }