@cartbase/storefront 0.2.0 → 0.4.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/package.json +1 -1
- package/src/api/carts.ts +5 -3
- package/src/api/checkout.ts +93 -43
- package/src/api/integrations.ts +4 -17
- package/src/cart-drawer/labels-bg.ts +1 -1
- package/src/cart-drawer/labels.ts +119 -119
- package/src/cart-drawer/summary-breakdown.tsx +197 -196
- package/src/checkout/checkout-client.tsx +13 -10
- package/src/checkout/index.ts +0 -1
- package/src/checkout/labels-bg.ts +1 -1
- package/src/checkout/labels.ts +263 -263
- package/src/checkout/order-summary.tsx +45 -45
- package/src/checkout/payment-method-list.tsx +92 -32
- package/src/checkout/use-checkout-orchestration.ts +100 -92
- package/src/lib/payment-constants.ts +53 -66
- package/src/order/labels-bg.ts +39 -39
- package/src/order/labels.ts +79 -79
- package/src/order/order-completed-template.tsx +17 -9
- package/src/order/order-payment-card.tsx +26 -35
- package/src/order/order-totals.tsx +250 -245
|
@@ -1,196 +1,197 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import type { AppliedGiftCard } from "../api/carts"
|
|
4
|
-
import { convertToLocale } from "../lib/money"
|
|
5
|
-
import { useCartDrawer } from "./context"
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* CartSummaryBreakdown — full totals breakdown for the drawer (or a cart
|
|
9
|
-
* page): subtotal, discount, shipping, tax, COD fee, total, applied
|
|
10
|
-
* gift-card tender and the remainder. Alternative to the compact
|
|
11
|
-
* `CartStickyFooter` (which deliberately shows a product-only subtotal in
|
|
12
|
-
* pre-checkout context).
|
|
13
|
-
*
|
|
14
|
-
* Ported from `@1click/ui/src/cart-drawer/summary-breakdown.tsx` (v2.3.1)
|
|
15
|
-
* and EXPANDED for Cartbase: the source rendered a client-summed product
|
|
16
|
-
* subtotal only; Cartbase's decorated cart carries server-computed totals on
|
|
17
|
-
* every read (`subtotal`, `discount_total`, `shipping_total`, `tax_total`,
|
|
18
|
-
* `
|
|
19
|
-
* `gift_card_remainder`, `total` — all EUR major units), so this component
|
|
20
|
-
* renders EXACTLY what the server returns and computes no money. The only
|
|
21
|
-
* client-side transformation is the display sign on deductions.
|
|
22
|
-
*
|
|
23
|
-
* Row rules (see `selectSummaryRows`):
|
|
24
|
-
* - discount / tax /
|
|
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
|
-
| { kind: "
|
|
52
|
-
| { kind: "
|
|
53
|
-
| { kind: "
|
|
54
|
-
| { kind: "
|
|
55
|
-
| { kind: "
|
|
56
|
-
| { kind: "
|
|
57
|
-
| { kind: "
|
|
58
|
-
| { kind: "
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
rows
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<span
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<span
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<span
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<span>{labels.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<span
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<span
|
|
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
|
-
<span
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { AppliedGiftCard } from "../api/carts"
|
|
4
|
+
import { convertToLocale } from "../lib/money"
|
|
5
|
+
import { useCartDrawer } from "./context"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CartSummaryBreakdown — full totals breakdown for the drawer (or a cart
|
|
9
|
+
* page): subtotal, discount, shipping, tax, COD fee, total, applied
|
|
10
|
+
* gift-card tender and the remainder. Alternative to the compact
|
|
11
|
+
* `CartStickyFooter` (which deliberately shows a product-only subtotal in
|
|
12
|
+
* pre-checkout context).
|
|
13
|
+
*
|
|
14
|
+
* Ported from `@1click/ui/src/cart-drawer/summary-breakdown.tsx` (v2.3.1)
|
|
15
|
+
* and EXPANDED for Cartbase: the source rendered a client-summed product
|
|
16
|
+
* subtotal only; Cartbase's decorated cart carries server-computed totals on
|
|
17
|
+
* every read (`subtotal`, `discount_total`, `shipping_total`, `tax_total`,
|
|
18
|
+
* `payment_method_fee_total`/`payment_method_fee_label`, `gift_cards[]`, `gift_card_total`,
|
|
19
|
+
* `gift_card_remainder`, `total` — all EUR major units), so this component
|
|
20
|
+
* renders EXACTLY what the server returns and computes no money. The only
|
|
21
|
+
* client-side transformation is the display sign on deductions.
|
|
22
|
+
*
|
|
23
|
+
* Row rules (see `selectSummaryRows`):
|
|
24
|
+
* - discount / tax / method-fee rows appear only when their total is > 0.
|
|
25
|
+
* - the method fee renders the server's `payment_method_fee_label`
|
|
26
|
+
* (labels.paymentMethodFee fallback).
|
|
27
|
+
* - shipping renders the amount once a shipping method is set (0 → FREE);
|
|
28
|
+
* before that it shows "calculated at checkout".
|
|
29
|
+
* - gift-card tender renders one row per applied card (masked `last4`,
|
|
30
|
+
* negative display) plus the `gift_card_remainder` row — the amount the
|
|
31
|
+
* remainder provider actually charges. A depleted card stays listed at 0
|
|
32
|
+
* (server keeps it in `gift_cards` at amount 0).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** The decorated-cart fields the breakdown consumes (structural subset). */
|
|
36
|
+
export type SummaryCartTotals = {
|
|
37
|
+
subtotal: number
|
|
38
|
+
discount_total: number
|
|
39
|
+
shipping_total: number
|
|
40
|
+
tax_total: number
|
|
41
|
+
total: number
|
|
42
|
+
payment_method_fee_total?: number
|
|
43
|
+
payment_method_fee_label?: string | null
|
|
44
|
+
gift_cards?: AppliedGiftCard[]
|
|
45
|
+
gift_card_total?: number
|
|
46
|
+
gift_card_remainder?: number
|
|
47
|
+
shipping_methods?: ReadonlyArray<unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type SummaryRow =
|
|
51
|
+
| { kind: "subtotal"; amount: number }
|
|
52
|
+
| { kind: "discount"; amount: number }
|
|
53
|
+
| { kind: "shipping"; amount: number }
|
|
54
|
+
| { kind: "shipping_pending" }
|
|
55
|
+
| { kind: "tax"; amount: number }
|
|
56
|
+
| { kind: "payment_method_fee"; amount: number; label: string | null }
|
|
57
|
+
| { kind: "total"; amount: number }
|
|
58
|
+
| { kind: "gift_card"; last4: string; amount: number }
|
|
59
|
+
| { kind: "remainder"; amount: number }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pure row selection over the decorated-cart totals — exported for unit
|
|
63
|
+
* tests. Amounts pass through verbatim (server truth); deduction rows
|
|
64
|
+
* (discount, gift cards) carry a negated amount for display only.
|
|
65
|
+
*/
|
|
66
|
+
export function selectSummaryRows(cart: SummaryCartTotals): SummaryRow[] {
|
|
67
|
+
const rows: SummaryRow[] = []
|
|
68
|
+
rows.push({ kind: "subtotal", amount: cart.subtotal })
|
|
69
|
+
|
|
70
|
+
if ((cart.discount_total ?? 0) > 0) {
|
|
71
|
+
rows.push({ kind: "discount", amount: -cart.discount_total })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (cart.shipping_methods && cart.shipping_methods.length > 0) {
|
|
75
|
+
rows.push({ kind: "shipping", amount: cart.shipping_total })
|
|
76
|
+
} else {
|
|
77
|
+
rows.push({ kind: "shipping_pending" })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if ((cart.tax_total ?? 0) > 0) {
|
|
81
|
+
rows.push({ kind: "tax", amount: cart.tax_total })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if ((cart.payment_method_fee_total ?? 0) > 0) {
|
|
85
|
+
rows.push({
|
|
86
|
+
kind: "payment_method_fee",
|
|
87
|
+
amount: cart.payment_method_fee_total!,
|
|
88
|
+
label: cart.payment_method_fee_label ?? null,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rows.push({ kind: "total", amount: cart.total })
|
|
93
|
+
|
|
94
|
+
if (cart.gift_cards && cart.gift_cards.length > 0) {
|
|
95
|
+
for (const card of cart.gift_cards) {
|
|
96
|
+
// `|| 0` normalizes -0 (a depleted card at amount 0) so Intl never
|
|
97
|
+
// renders a stray "-€0.00".
|
|
98
|
+
rows.push({ kind: "gift_card", last4: card.last4, amount: -card.amount || 0 })
|
|
99
|
+
}
|
|
100
|
+
rows.push({ kind: "remainder", amount: cart.gift_card_remainder ?? 0 })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return rows
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function CartSummaryBreakdown() {
|
|
107
|
+
const { cart, labels } = useCartDrawer()
|
|
108
|
+
if (!cart) return null
|
|
109
|
+
|
|
110
|
+
const currencyCode = cart.currency_code
|
|
111
|
+
const money = (amount: number) =>
|
|
112
|
+
convertToLocale({ amount, currency_code: currencyCode })
|
|
113
|
+
const rows = selectSummaryRows(cart)
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="px-6 py-5 space-y-2">
|
|
117
|
+
{rows.map((row, i) => {
|
|
118
|
+
switch (row.kind) {
|
|
119
|
+
case "subtotal":
|
|
120
|
+
return (
|
|
121
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
|
|
122
|
+
<span>{labels.subtotal}</span>
|
|
123
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
case "discount":
|
|
127
|
+
return (
|
|
128
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-success">
|
|
129
|
+
<span>{labels.discount}</span>
|
|
130
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
131
|
+
</div>
|
|
132
|
+
)
|
|
133
|
+
case "shipping":
|
|
134
|
+
return (
|
|
135
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
|
|
136
|
+
<span>{labels.shipping}</span>
|
|
137
|
+
<span className="tabular-nums">
|
|
138
|
+
{row.amount === 0 ? labels.free : money(row.amount)}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
case "shipping_pending":
|
|
143
|
+
return (
|
|
144
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
|
|
145
|
+
<span>{labels.shipping}</span>
|
|
146
|
+
<span>{labels.calculatedAtCheckout}</span>
|
|
147
|
+
</div>
|
|
148
|
+
)
|
|
149
|
+
case "tax":
|
|
150
|
+
return (
|
|
151
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
|
|
152
|
+
<span>{labels.tax}</span>
|
|
153
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
case "payment_method_fee":
|
|
157
|
+
return (
|
|
158
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-muted-foreground">
|
|
159
|
+
<span>{row.label ?? labels.paymentMethodFee}</span>
|
|
160
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
case "total":
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
key={i}
|
|
167
|
+
className="flex justify-between items-center pt-2 mt-1 border-t border-border"
|
|
168
|
+
>
|
|
169
|
+
<span className="text-[15px] font-bold text-foreground">
|
|
170
|
+
{labels.total}
|
|
171
|
+
</span>
|
|
172
|
+
<span className="text-[17px] font-bold text-foreground tracking-tight tabular-nums">
|
|
173
|
+
{money(row.amount)}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
case "gift_card":
|
|
178
|
+
return (
|
|
179
|
+
<div key={i} className="flex justify-between items-center text-[13px] text-success">
|
|
180
|
+
<span>
|
|
181
|
+
{labels.giftCard} ••{row.last4}
|
|
182
|
+
</span>
|
|
183
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
case "remainder":
|
|
187
|
+
return (
|
|
188
|
+
<div key={i} className="flex justify-between items-center text-[13px] font-semibold text-foreground">
|
|
189
|
+
<span>{labels.remainingToPay}</span>
|
|
190
|
+
<span className="tabular-nums">{money(row.amount)}</span>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
})}
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
@@ -13,7 +13,6 @@ import { CheckoutShippingMethodList } from "./shipping-method-list"
|
|
|
13
13
|
import { PaymentWrapper } from "./payment-wrapper"
|
|
14
14
|
import {
|
|
15
15
|
useCheckoutOrchestration,
|
|
16
|
-
type CheckoutCodConfig,
|
|
17
16
|
type CheckoutLogError,
|
|
18
17
|
type PaymentProviderLike,
|
|
19
18
|
} from "./use-checkout-orchestration"
|
|
@@ -35,9 +34,9 @@ import {
|
|
|
35
34
|
* - The listings arrive as SDK DTOs: `listShippingOptions` /
|
|
36
35
|
* `listPaymentProviders` (both rule-filtered server-side) — fetch them
|
|
37
36
|
* in the host and pass down (docs/storefront/checkout.md).
|
|
38
|
-
* -
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* - Fee prediction is NEVER hardcoded: the payment listing entries carry
|
|
38
|
+
* each method's own `fee_amount`/`fee_label` (the integrations `cod`
|
|
39
|
+
* block died 2026-08-11 — any method may carry a fee).
|
|
41
40
|
* - `onCartChange` closes the loop for summary mutations: the decorated
|
|
42
41
|
* cart returned by promo/gift-card/quantity calls is handed up so the
|
|
43
42
|
* host updates its cart state (`router.refresh()` in an RSC app) AND
|
|
@@ -61,8 +60,6 @@ type CheckoutClientProps = {
|
|
|
61
60
|
methods: PaymentProviderLike[] | null,
|
|
62
61
|
selectedShippingOption: StoreShippingOption | null
|
|
63
62
|
) => PaymentProviderLike[] | null
|
|
64
|
-
/** The integrations config `cod` block — drives the optimistic COD fee. */
|
|
65
|
-
codConfig?: CheckoutCodConfig
|
|
66
63
|
/** Show the gift-card widget in the summary (default true). */
|
|
67
64
|
showGiftCards?: boolean
|
|
68
65
|
logoByFulfillmentOptionId?: Record<string, { src: string; alt: string }>
|
|
@@ -81,6 +78,12 @@ type CheckoutClientProps = {
|
|
|
81
78
|
logError?: CheckoutLogError
|
|
82
79
|
/** Receives every decorated cart returned by a summary mutation. */
|
|
83
80
|
onCartChange?: (cart: Cart) => void
|
|
81
|
+
/**
|
|
82
|
+
* Extra payment tabs forwarded to CheckoutPaymentMethodList — the seam
|
|
83
|
+
* for embedded third-party providers rendered in the same radio-card
|
|
84
|
+
* style as card/cod. See the prop's JSDoc there.
|
|
85
|
+
*/
|
|
86
|
+
extraPaymentTabs?: Array<{ id: string; label: string; content: React.ReactNode }>
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
export function CheckoutClient({
|
|
@@ -92,7 +95,6 @@ export function CheckoutClient({
|
|
|
92
95
|
countryCode,
|
|
93
96
|
countries,
|
|
94
97
|
paymentMethodFilter,
|
|
95
|
-
codConfig,
|
|
96
98
|
showGiftCards = true,
|
|
97
99
|
logoByFulfillmentOptionId,
|
|
98
100
|
appearance,
|
|
@@ -102,6 +104,7 @@ export function CheckoutClient({
|
|
|
102
104
|
resolveTrackingMetadata,
|
|
103
105
|
logError,
|
|
104
106
|
onCartChange,
|
|
107
|
+
extraPaymentTabs,
|
|
105
108
|
}: CheckoutClientProps) {
|
|
106
109
|
const o = useCheckoutOrchestration({
|
|
107
110
|
client,
|
|
@@ -112,7 +115,6 @@ export function CheckoutClient({
|
|
|
112
115
|
countryCode,
|
|
113
116
|
countries,
|
|
114
117
|
paymentMethodFilter,
|
|
115
|
-
codConfig,
|
|
116
118
|
orderConfirmedPath,
|
|
117
119
|
onOrderPlaced,
|
|
118
120
|
resolveTrackingMetadata,
|
|
@@ -195,6 +197,7 @@ export function CheckoutClient({
|
|
|
195
197
|
}
|
|
196
198
|
total={o.optimisticTotal}
|
|
197
199
|
logError={logError}
|
|
200
|
+
extraTabs={extraPaymentTabs}
|
|
198
201
|
/>
|
|
199
202
|
</div>
|
|
200
203
|
|
|
@@ -208,8 +211,8 @@ export function CheckoutClient({
|
|
|
208
211
|
onOptimisticShippingClear={() =>
|
|
209
212
|
o.setOptimisticShippingCost(null)
|
|
210
213
|
}
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
optimisticMethodFee={o.optimisticMethodFee}
|
|
215
|
+
onOptimisticMethodFeeClear={() => o.setOptimisticMethodFee(null)}
|
|
213
216
|
showGiftCards={showGiftCards}
|
|
214
217
|
onCartChange={handleCartChange}
|
|
215
218
|
/>
|
package/src/checkout/index.ts
CHANGED
|
@@ -73,7 +73,7 @@ export const bulgarianCheckoutLabels: CheckoutLabels = {
|
|
|
73
73
|
discount: "Отстъпка",
|
|
74
74
|
discountCode: "Код за отстъпка",
|
|
75
75
|
discountSub: "Спестете с промо код",
|
|
76
|
-
|
|
76
|
+
paymentMethodFee: "Такса за плащане",
|
|
77
77
|
recommended: "Препоръчано за вас",
|
|
78
78
|
recommendedSoon: "Скоро тук",
|
|
79
79
|
recommendedSoonSub: "Персонализирани предложения за вас",
|