@cartbase/storefront 0.1.2 → 0.3.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.
- package/README.md +3 -3
- package/package.json +231 -226
- package/src/api/auth.ts +1 -1
- package/src/api/carts.ts +5 -3
- package/src/api/checkout.ts +91 -40
- package/src/api/consent.ts +2 -2
- package/src/api/integrations.ts +4 -17
- package/src/api/regions.ts +1 -1
- package/src/cart-drawer/context.tsx +4 -4
- package/src/cart-drawer/cross-sell-carousel.tsx +1 -1
- package/src/cart-drawer/cross-sell-sidebar.tsx +1 -1
- package/src/cart-drawer/header.tsx +1 -1
- package/src/cart-drawer/item/index.tsx +2 -2
- package/src/cart-drawer/item/quantity.tsx +2 -2
- package/src/cart-drawer/item/upsell.tsx +3 -3
- package/src/cart-drawer/item/variant.tsx +2 -2
- package/src/cart-drawer/labels-bg.ts +2 -2
- package/src/cart-drawer/labels.ts +119 -119
- package/src/cart-drawer/summary-breakdown.tsx +197 -196
- package/src/cart-drawer/template.tsx +2 -2
- package/src/cart-drawer/tiered-progress.tsx +1 -1
- package/src/checkout/address-error-copy.ts +3 -3
- package/src/checkout/address-form.tsx +2 -2
- package/src/checkout/address-select.tsx +1 -1
- package/src/checkout/boxnow-locker-selector.tsx +3 -3
- package/src/checkout/checkout-client.tsx +15 -12
- package/src/checkout/company-details.tsx +1 -1
- package/src/checkout/compare-addresses.ts +1 -1
- package/src/checkout/discount-section.tsx +5 -5
- package/src/checkout/econt-office-selector.tsx +2 -2
- package/src/checkout/gift-card-section.tsx +2 -2
- package/src/checkout/index.ts +0 -1
- package/src/checkout/labels-bg.ts +2 -2
- package/src/checkout/labels.ts +263 -263
- package/src/checkout/line-item-card.tsx +2 -2
- package/src/checkout/order-summary.tsx +48 -48
- package/src/checkout/payment-button.tsx +4 -4
- package/src/checkout/payment-error-copy.ts +6 -6
- package/src/checkout/payment-method-list.tsx +93 -33
- package/src/checkout/payment-wrapper.tsx +8 -11
- package/src/checkout/promotion-error-copy.ts +2 -2
- package/src/checkout/shipping-method-list.tsx +2 -2
- package/src/checkout/stripe-wrapper.tsx +1 -1
- package/src/checkout/use-checkout-orchestration.ts +108 -98
- package/src/common/cart-button-client.tsx +1 -1
- package/src/common/cart-button.tsx +1 -1
- package/src/common/country-select.tsx +2 -2
- package/src/common/delete-button.tsx +1 -1
- package/src/common/index.ts +1 -1
- package/src/common/language-select.tsx +2 -2
- package/src/common/localized-link.tsx +1 -1
- package/src/lib/cart-helpers.ts +2 -2
- package/src/lib/cookie-names.ts +41 -0
- package/src/lib/get-product-price.ts +3 -3
- package/src/lib/payment-constants.ts +53 -66
- package/src/lib/store-api-error.ts +1 -1
- package/src/order/index.ts +1 -1
- package/src/order/labels-bg.ts +39 -39
- package/src/order/labels.ts +79 -79
- package/src/order/order-address-card.tsx +1 -1
- package/src/order/order-completed-template.tsx +20 -12
- package/src/order/order-confirmation-header.tsx +1 -1
- package/src/order/order-delivery-card.tsx +3 -3
- package/src/order/order-item.tsx +2 -2
- package/src/order/order-items-list.tsx +3 -3
- package/src/order/order-payment-card.tsx +27 -36
- package/src/order/order-timeline.tsx +2 -2
- package/src/order/order-totals.tsx +250 -245
- package/src/platform/identity.ts +18 -0
- package/src/platform/index.ts +8 -0
- package/src/platform/metadata.ts +26 -0
- package/src/platform/platform-init.tsx +46 -0
- package/src/products/image-gallery.tsx +1 -1
- package/src/products/mobile-actions.tsx +1 -1
- package/src/products/option-select.tsx +1 -1
- package/src/products/preview-price.tsx +1 -1
- package/src/products/product-actions.tsx +6 -6
- package/src/products/product-info.tsx +1 -1
- package/src/products/product-preview.tsx +1 -1
- package/src/products/product-price.tsx +2 -2
- package/src/products/product-tabs.tsx +1 -1
- package/src/products/purchase-options.tsx +1 -1
- package/src/products/related-products.tsx +3 -3
- package/src/products/variant-matching.ts +4 -4
- package/src/reviews-ui/labels.ts +1 -1
- package/src/reviews-ui/photo-upload.tsx +1 -1
- package/src/reviews-ui/review-list.tsx +1 -1
- package/src/reviews-ui/review-widget.tsx +1 -1
- package/src/reviews-ui/review-wizard.tsx +1 -1
- package/src/reviews-ui/wizard-state.ts +1 -1
- package/src/store/category-template.tsx +2 -2
- package/src/store/collection-template.tsx +2 -2
- package/src/store/labels.ts +2 -2
- package/src/store/paginated-products.tsx +1 -1
- package/src/store/search-params.ts +1 -1
- package/src/store/search-template.tsx +1 -1
- package/src/tracking/consent-banner.tsx +1 -1
- package/src/tracking/consent-init.tsx +1 -1
- package/src/tracking/consent.ts +6 -6
- package/src/tracking/fbq.ts +2 -2
- package/src/tracking/get-tracking-attribution.ts +1 -1
- package/src/tracking/get-tracking-config.ts +2 -2
- package/src/tracking/index.ts +2 -2
- package/src/tracking/meta-pixel.tsx +1 -1
- package/src/tracking/rybbit.tsx +1 -1
- package/src/tracking/types.ts +6 -6
|
@@ -1,58 +1,48 @@
|
|
|
1
1
|
import { CreditCard } from "lucide-react"
|
|
2
|
-
import {
|
|
3
|
-
isManual,
|
|
4
|
-
isPaypal,
|
|
5
|
-
isStripeLike,
|
|
6
|
-
paymentInfoMap,
|
|
7
|
-
} from "../lib/payment-constants"
|
|
2
|
+
import { isPaypal, isStripeLike, paymentInfoMap } from "../lib/payment-constants"
|
|
8
3
|
import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Payment-method card. Ported from
|
|
12
7
|
* `@1click/ui/src/order/order-payment-card.tsx` (v2.3.1); data seam:
|
|
13
8
|
* Medusa's `order.payment_collections[0].payments[0]` embed does not exist
|
|
14
|
-
* on
|
|
15
|
-
* surface) — the
|
|
16
|
-
* `prepareCheckout()
|
|
17
|
-
*
|
|
9
|
+
* on Cartbase's `StoreOrderDetail` (payment internals never cross the store
|
|
10
|
+
* surface) — the tender arrives via props instead: from
|
|
11
|
+
* `prepareCheckout()`, the decorated cart's `payment_collection` sessions,
|
|
12
|
+
* or the app's checkout state.
|
|
18
13
|
*/
|
|
19
14
|
|
|
20
15
|
/**
|
|
21
|
-
* Resolve a human-readable, locale-correct title for a payment
|
|
22
|
-
*
|
|
16
|
+
* Resolve a human-readable, locale-correct title for a payment tender.
|
|
17
|
+
* Since the pp_* kill (20260811250000) a merchant METHOD carries its own
|
|
18
|
+
* merchant-written name — that name IS the display (Bank transfer, the
|
|
19
|
+
* merchant's COD name); locale buckets apply to processor ids only:
|
|
23
20
|
*
|
|
24
|
-
* 1.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* adopted the labels pack yet.
|
|
31
|
-
* 3. The raw provider id as a last resort, so the card never renders
|
|
32
|
-
* empty if a brand-new provider lands.
|
|
33
|
-
*
|
|
34
|
-
* The bucket is determined by the same `isStripeLike` / `isManual` /
|
|
35
|
-
* `isPaypal` predicates used everywhere else in the library (translated to
|
|
36
|
-
* barter ids in `lib/payment-constants`), so a single change to those
|
|
37
|
-
* propagates here automatically.
|
|
21
|
+
* 1. `methodName` (the method's merchant name), verbatim.
|
|
22
|
+
* 2. Bucketed lookup against `labels.paymentMethodTitles` for processor
|
|
23
|
+
* ids (Stripe → the locale's "card" title, PayPal likewise).
|
|
24
|
+
* 3. Hardcoded `paymentInfoMap` fallback for known processor ids.
|
|
25
|
+
* 4. The raw provider id as a last resort, so the card never renders
|
|
26
|
+
* empty if a brand-new processor lands.
|
|
38
27
|
*/
|
|
39
28
|
export function resolvePaymentTitle(
|
|
40
|
-
providerId: string | undefined,
|
|
41
|
-
paymentMethodTitles: OrderLabels["paymentMethodTitles"]
|
|
29
|
+
providerId: string | null | undefined,
|
|
30
|
+
paymentMethodTitles: OrderLabels["paymentMethodTitles"],
|
|
31
|
+
methodName?: string | null
|
|
42
32
|
): string {
|
|
33
|
+
if (methodName) return methodName
|
|
43
34
|
if (!providerId) return ""
|
|
44
35
|
if (isStripeLike(providerId)) return paymentMethodTitles.card
|
|
45
|
-
// COD before manual: pp_cod is its own bucket (payments-registry card) —
|
|
46
|
-
// an offline manual payment must never render as "Cash on delivery".
|
|
47
|
-
if (providerId === "pp_cod") return paymentMethodTitles.cod
|
|
48
|
-
if (isManual(providerId)) return paymentMethodTitles.manual
|
|
49
36
|
if (isPaypal(providerId)) return paymentMethodTitles.paypal
|
|
50
37
|
return paymentInfoMap[providerId]?.title ?? providerId
|
|
51
38
|
}
|
|
52
39
|
|
|
53
40
|
type OrderPaymentCardProps = {
|
|
54
|
-
/**
|
|
41
|
+
/** Processor id (e.g. `pp_stripe`), or null for a merchant method. */
|
|
55
42
|
providerId?: string | null
|
|
43
|
+
/** The merchant method's name (Bank transfer, the merchant's COD name) —
|
|
44
|
+
* wins over any provider bucket when present. */
|
|
45
|
+
methodName?: string | null
|
|
56
46
|
/** Last 4 card digits when known (Stripe checkout state). */
|
|
57
47
|
cardLast4?: string | null
|
|
58
48
|
labels?: Pick<OrderLabels, "paymentMethod" | "paymentMethodTitles">
|
|
@@ -60,15 +50,16 @@ type OrderPaymentCardProps = {
|
|
|
60
50
|
|
|
61
51
|
export function OrderPaymentCard({
|
|
62
52
|
providerId,
|
|
53
|
+
methodName,
|
|
63
54
|
cardLast4,
|
|
64
55
|
labels,
|
|
65
56
|
}: OrderPaymentCardProps) {
|
|
66
57
|
const l = { ...defaultOrderLabels, ...labels }
|
|
67
58
|
|
|
68
|
-
if (!providerId) return null
|
|
59
|
+
if (!providerId && !methodName) return null
|
|
69
60
|
|
|
70
|
-
const title = resolvePaymentTitle(providerId, l.paymentMethodTitles)
|
|
71
|
-
const icon = paymentInfoMap[providerId]?.icon
|
|
61
|
+
const title = resolvePaymentTitle(providerId, l.paymentMethodTitles, methodName)
|
|
62
|
+
const icon = (providerId && paymentInfoMap[providerId]?.icon) || null
|
|
72
63
|
|
|
73
64
|
return (
|
|
74
65
|
<div className="bg-card rounded-xl p-5 shadow-sm">
|
|
@@ -6,14 +6,14 @@ import { defaultOrderLabels, type OrderLabels } from "./labels"
|
|
|
6
6
|
/**
|
|
7
7
|
* Fulfillment progress timeline. Ported from
|
|
8
8
|
* `@1click/ui/src/order/order-timeline.tsx` (v2.3.1) — the step state
|
|
9
|
-
* machine and rendering are unchanged.
|
|
9
|
+
* machine and rendering are unchanged. Cartbase seam: there is no
|
|
10
10
|
* `order.fulfillment_status` column on the store surface; derive the
|
|
11
11
|
* status from the fulfillments' lifecycle timestamps with
|
|
12
12
|
* `deriveFulfillmentStatus(order.fulfillments)` and pass it in.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Derive a Medusa-style fulfillment status from
|
|
16
|
+
* Derive a Medusa-style fulfillment status from Cartbase's fulfillment
|
|
17
17
|
* lifecycle timestamps (packed_at → fulfilled, shipped_at → shipped,
|
|
18
18
|
* delivered_at → delivered; canceled fulfillments are ignored). Returns
|
|
19
19
|
* `undefined` for "not fulfilled yet" (timeline shows "Placed"). Partial
|
|
@@ -1,245 +1,250 @@
|
|
|
1
|
-
import { DualPrice } from "../lib/dual-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
|
|
8
|
-
*
|
|
9
|
-
* Totals are SERVER truth and the
|
|
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`, `
|
|
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
|
-
*
|
|
21
|
-
* 1.
|
|
22
|
-
* already folded into `total` server-side), labeled by the
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
source.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1
|
+
import { DualPrice } from "../lib/dual-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
|
+
<DualPrice
|
|
221
|
+
amount={row.amount}
|
|
222
|
+
currencyCode={currencyCode}
|
|
223
|
+
className="text-sm text-success"
|
|
224
|
+
/>
|
|
225
|
+
</span>
|
|
226
|
+
) : (
|
|
227
|
+
<DualPrice
|
|
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
|
+
<DualPrice
|
|
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 }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform identity — MIRROR of `src/lib/platform/identity.ts` in the main
|
|
3
|
+
* app (docs/cards/platform-fingerprints.md). This library publishes to npm
|
|
4
|
+
* independently, so it cannot import the app's root `package.json` across
|
|
5
|
+
* the package boundary — it reads its OWN `package.json` instead.
|
|
6
|
+
*
|
|
7
|
+
* Sync obligation: if `PLATFORM_NAME` ever changes, update BOTH copies in
|
|
8
|
+
* the same commit. There is no automated link between them.
|
|
9
|
+
*
|
|
10
|
+
* Emitters that consume this: `./metadata.ts` (generator meta tag),
|
|
11
|
+
* `./platform-init.tsx` (window.Cartbase).
|
|
12
|
+
*/
|
|
13
|
+
import pkg from "../../package.json"
|
|
14
|
+
|
|
15
|
+
export const PLATFORM_NAME = "Cartbase"
|
|
16
|
+
|
|
17
|
+
/** This package's own published version. */
|
|
18
|
+
export const PLATFORM_VERSION: string = pkg.version
|