@cartbase/storefront 0.17.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 (107) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +258 -256
  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 -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/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/gift-wrap.tsx +82 -82
  28. package/src/cart-drawer/index.ts +71 -68
  29. package/src/cart-drawer/item/index.tsx +162 -162
  30. package/src/cart-drawer/item/quantity.tsx +8 -14
  31. package/src/cart-drawer/item/upsell.tsx +110 -110
  32. package/src/cart-drawer/labels.ts +123 -123
  33. package/src/cart-drawer/mutation-queue.ts +78 -0
  34. package/src/cart-drawer/notes.tsx +131 -131
  35. package/src/cart-drawer/sticky-footer.tsx +73 -73
  36. package/src/cart-drawer/summary-breakdown.tsx +197 -197
  37. package/src/checkout/address-error-copy.ts +117 -117
  38. package/src/checkout/boxnow-locker-selector.tsx +410 -410
  39. package/src/checkout/compare-addresses.ts +40 -40
  40. package/src/checkout/discount-section.tsx +218 -218
  41. package/src/checkout/error-copy-codes.ts +63 -63
  42. package/src/checkout/geocode.ts +154 -154
  43. package/src/checkout/gift-card-section.tsx +224 -224
  44. package/src/checkout/index.ts +72 -72
  45. package/src/checkout/labels.ts +495 -495
  46. package/src/checkout/payment-button.tsx +372 -372
  47. package/src/checkout/payment-error-copy.ts +154 -154
  48. package/src/checkout/promotion-error-copy.ts +91 -91
  49. package/src/checkout/use-checkout-orchestration.ts +1592 -1592
  50. package/src/common/country-flag.tsx +52 -52
  51. package/src/common/country-select.tsx +11 -11
  52. package/src/common/index.ts +20 -20
  53. package/src/common/market-select.tsx +57 -57
  54. package/src/index.ts +12 -12
  55. package/src/lib/country-name.ts +59 -59
  56. package/src/lib/get-product-price.ts +133 -133
  57. package/src/lib/media-image.tsx +39 -39
  58. package/src/lib/payment-constants.ts +53 -53
  59. package/src/lib/platform.ts +13 -13
  60. package/src/lib/price.tsx +39 -39
  61. package/src/lib/store-api-error.ts +36 -36
  62. package/src/lib/variant-caption.ts +32 -32
  63. package/src/locales/bg.ts +467 -467
  64. package/src/locales/context.ts +37 -37
  65. package/src/locales/en.ts +26 -26
  66. package/src/locales/es.ts +466 -466
  67. package/src/locales/index.ts +19 -19
  68. package/src/locales/provider.tsx +59 -59
  69. package/src/locales/types.ts +77 -77
  70. package/src/order/index.ts +62 -62
  71. package/src/order/labels.ts +79 -79
  72. package/src/order/order-totals.tsx +250 -250
  73. package/src/primitives/select-field.tsx +93 -93
  74. package/src/products/image-gallery.tsx +43 -43
  75. package/src/products/index.ts +68 -59
  76. package/src/products/option-select.tsx +67 -67
  77. package/src/products/product-actions-wrapper.tsx +63 -62
  78. package/src/products/product-actions.tsx +151 -150
  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/product-template.tsx +163 -162
  84. package/src/products/purchase-options.tsx +130 -130
  85. package/src/products/sets.ts +110 -0
  86. package/src/products/use-product-actions.ts +199 -164
  87. package/src/products/variant-matching.ts +71 -71
  88. package/src/products/variant-url.ts +74 -74
  89. package/src/reviews-ui/index.ts +73 -73
  90. package/src/store/category-template.tsx +136 -136
  91. package/src/store/index.ts +40 -40
  92. package/src/tracking/chatgpt-pixel.tsx +99 -99
  93. package/src/tracking/consent-init.tsx +62 -62
  94. package/src/tracking/events.ts +348 -348
  95. package/src/tracking/ga4.tsx +93 -93
  96. package/src/tracking/google-ads.ts +84 -84
  97. package/src/tracking/gtm.tsx +60 -60
  98. package/src/tracking/inline-script.ts +49 -49
  99. package/src/tracking/oaiq.ts +206 -206
  100. package/src/tracking/tiktok-pixel.tsx +91 -91
  101. package/src/tracking/track-init.tsx +56 -56
  102. package/src/tracking/track-order-purchase.tsx +122 -122
  103. package/src/tracking/ttq.ts +180 -180
  104. package/src/tracking/use-tracking-config.ts +54 -54
  105. package/theme/index.css +25 -25
  106. package/theme/theme.css +150 -150
  107. package/theme/tokens.css +106 -106
@@ -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 }