@cartbase/storefront 0.2.0 → 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/package.json +1 -1
- package/src/api/carts.ts +5 -3
- package/src/api/checkout.ts +91 -40
- 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 +96 -86
- 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
|
@@ -19,12 +19,12 @@ import { GiftCardSection } from "./gift-card-section"
|
|
|
19
19
|
* Ported from `@1click/ui/src/checkout/order-summary.tsx` (v2.3.1) with
|
|
20
20
|
* the Cartbase data seam:
|
|
21
21
|
*
|
|
22
|
-
* -
|
|
23
|
-
* `cart.
|
|
24
|
-
* while a live
|
|
25
|
-
* NOT a metadata-flagged fee line item.
|
|
26
|
-
* read those fields; `isProductLine` is
|
|
27
|
-
* any legacy fee-line carts.
|
|
22
|
+
* - The payment method fee is CART-LEVEL decoration on the Cartbase wire —
|
|
23
|
+
* `cart.payment_method_fee_total` / `payment_method_fee_label`, folded
|
|
24
|
+
* into `cart.total` while a live method session exists
|
|
25
|
+
* (docs/storefront/checkout.md) — NOT a metadata-flagged fee line item.
|
|
26
|
+
* The fee row + optimistic math read those fields; `isProductLine` is
|
|
27
|
+
* kept as a defensive filter for any legacy fee-line carts.
|
|
28
28
|
* - Gift-card tender rows (Cartbase feature): `gift_card_total` renders as
|
|
29
29
|
* a negative tender row and `gift_card_remainder` as "Remaining to pay"
|
|
30
30
|
* UNDER the (unchanged) total — tender never moves totals
|
|
@@ -211,22 +211,22 @@ type OrderSummaryProps = {
|
|
|
211
211
|
optimisticShippingCost: number | null
|
|
212
212
|
onOptimisticShippingClear?: () => void
|
|
213
213
|
/**
|
|
214
|
-
* Optimistic
|
|
214
|
+
* Optimistic method-fee prediction. Three values:
|
|
215
215
|
* - null → no prediction; render whatever the cart says.
|
|
216
|
-
* - 0 → predict no fee (toggling
|
|
216
|
+
* - 0 → predict no fee (toggling to a fee-less tender).
|
|
217
217
|
* - positive → predict the fee at this amount.
|
|
218
|
-
* Pass `useCheckoutOrchestration`'s `
|
|
219
|
-
* the deferred-checkout architecture defers the
|
|
220
|
-
* the
|
|
218
|
+
* Pass `useCheckoutOrchestration`'s `optimisticMethodFee`. Needed because
|
|
219
|
+
* the deferred-checkout architecture defers the method session (and so
|
|
220
|
+
* the fee) until Buy click.
|
|
221
221
|
*/
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
optimisticMethodFee?: number | null
|
|
223
|
+
onOptimisticMethodFeeClear?: () => void
|
|
224
224
|
/**
|
|
225
|
-
* Override label for the
|
|
226
|
-
*
|
|
227
|
-
*
|
|
225
|
+
* Override label for the payment-method fee row. Optional — falls back
|
|
226
|
+
* to the cart's server-provided `payment_method_fee_label` (the method's
|
|
227
|
+
* own fee_label), then to `labels.paymentMethodFee`.
|
|
228
228
|
*/
|
|
229
|
-
|
|
229
|
+
methodFeeLabel?: string
|
|
230
230
|
/** Show the gift-card widget. Default true; pass false for stores that
|
|
231
231
|
* haven't enabled gift cards (the widget itself is harmless either way —
|
|
232
232
|
* an unknown code gets the generic refusal). */
|
|
@@ -246,9 +246,9 @@ export function OrderSummary({
|
|
|
246
246
|
cart,
|
|
247
247
|
optimisticShippingCost,
|
|
248
248
|
onOptimisticShippingClear,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
optimisticMethodFee,
|
|
250
|
+
onOptimisticMethodFeeClear,
|
|
251
|
+
methodFeeLabel,
|
|
252
252
|
showGiftCards = true,
|
|
253
253
|
onCartChange,
|
|
254
254
|
}: OrderSummaryProps) {
|
|
@@ -268,39 +268,39 @@ export function OrderSummary({
|
|
|
268
268
|
optimisticShippingCost !== null ? optimisticShippingCost : cart.shipping_total
|
|
269
269
|
const shippingKnown = shippingCost !== null && shippingCost !== undefined
|
|
270
270
|
|
|
271
|
-
// Effective
|
|
272
|
-
// up. Three states (null / 0 / positive).
|
|
273
|
-
// `cart.
|
|
274
|
-
//
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
?
|
|
279
|
-
:
|
|
271
|
+
// Effective method fee: optimistic prediction wins until the cart
|
|
272
|
+
// catches up. Three states (null / 0 / positive). Server truth is
|
|
273
|
+
// `cart.payment_method_fee_total` (folded into cart.total while a live
|
|
274
|
+
// method session exists — docs/storefront/checkout.md).
|
|
275
|
+
const realMethodFeeAmount = cart.payment_method_fee_total ?? 0
|
|
276
|
+
const effectiveMethodFee =
|
|
277
|
+
optimisticMethodFee !== null && optimisticMethodFee !== undefined
|
|
278
|
+
? optimisticMethodFee
|
|
279
|
+
: realMethodFeeAmount
|
|
280
280
|
|
|
281
281
|
const displayTotal = useMemo(() => {
|
|
282
282
|
let total = cart.total ?? 0
|
|
283
283
|
if (optimisticShippingCost !== null) {
|
|
284
284
|
total = total - (cart.shipping_total ?? 0) + optimisticShippingCost
|
|
285
285
|
}
|
|
286
|
-
if (
|
|
287
|
-
total = total -
|
|
286
|
+
if (optimisticMethodFee !== null && optimisticMethodFee !== undefined) {
|
|
287
|
+
total = total - realMethodFeeAmount + effectiveMethodFee
|
|
288
288
|
}
|
|
289
289
|
return total
|
|
290
290
|
}, [
|
|
291
291
|
cart.total,
|
|
292
292
|
cart.shipping_total,
|
|
293
293
|
optimisticShippingCost,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
optimisticMethodFee,
|
|
295
|
+
effectiveMethodFee,
|
|
296
|
+
realMethodFeeAmount,
|
|
297
297
|
])
|
|
298
298
|
|
|
299
|
-
// Subtotal: Cartbase's `item_total` is products-only (the
|
|
299
|
+
// Subtotal: Cartbase's `item_total` is products-only (the fee is not a
|
|
300
300
|
// line item), so no fee subtraction is needed — direct server truth.
|
|
301
301
|
const productSubtotal = cart.item_total ?? 0
|
|
302
302
|
|
|
303
|
-
const
|
|
303
|
+
const methodFeeAmount = effectiveMethodFee
|
|
304
304
|
|
|
305
305
|
// Gift-card tender decoration — SERVER truth, rendered as-is. Totals
|
|
306
306
|
// never move; the tender rows sit under the total.
|
|
@@ -317,13 +317,13 @@ export function OrderSummary({
|
|
|
317
317
|
}
|
|
318
318
|
}, [cart.shipping_total, optimisticShippingCost, onOptimisticShippingClear])
|
|
319
319
|
|
|
320
|
-
// Clear optimistic
|
|
321
|
-
// prediction (
|
|
320
|
+
// Clear the optimistic fee once the cart's decoration matches the
|
|
321
|
+
// prediction (payment_method_fee_total, not a fee line item).
|
|
322
322
|
useEffect(() => {
|
|
323
|
-
if (
|
|
324
|
-
const synced =
|
|
325
|
-
if (synced)
|
|
326
|
-
}, [
|
|
323
|
+
if (optimisticMethodFee === null || optimisticMethodFee === undefined) return
|
|
324
|
+
const synced = realMethodFeeAmount === optimisticMethodFee
|
|
325
|
+
if (synced) onOptimisticMethodFeeClear?.()
|
|
326
|
+
}, [optimisticMethodFee, realMethodFeeAmount, onOptimisticMethodFeeClear])
|
|
327
327
|
|
|
328
328
|
return (
|
|
329
329
|
<div>
|
|
@@ -404,13 +404,13 @@ export function OrderSummary({
|
|
|
404
404
|
)}
|
|
405
405
|
</div>
|
|
406
406
|
|
|
407
|
-
{
|
|
407
|
+
{methodFeeAmount > 0 && (
|
|
408
408
|
<div className="flex justify-between text-sm">
|
|
409
409
|
<span className="text-muted-foreground">
|
|
410
|
-
{
|
|
410
|
+
{methodFeeLabel ?? cart.payment_method_fee_label ?? labels.paymentMethodFee}
|
|
411
411
|
</span>
|
|
412
412
|
<DualPrice
|
|
413
|
-
amount={
|
|
413
|
+
amount={methodFeeAmount}
|
|
414
414
|
currencyCode={cart.currency_code}
|
|
415
415
|
className="font-medium text-foreground"
|
|
416
416
|
/>
|
|
@@ -21,13 +21,14 @@ import type { CheckoutLogError } from "./use-checkout-orchestration"
|
|
|
21
21
|
* Ported from `@1click/ui/src/checkout/payment-method-list.tsx` (v2.3.1)
|
|
22
22
|
* with the Cartbase data seam: the `cart` prop is structural, and the
|
|
23
23
|
* `logCheckoutError` Supabase writer becomes the optional `logError`
|
|
24
|
-
* callback.
|
|
25
|
-
* `pp_stripe
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
24
|
+
* callback. Tender semantics on the Cartbase wire (pp_* kill): the "card"
|
|
25
|
+
* tab is the connected processor (`pp_stripe`); the "cod" tab is a
|
|
26
|
+
* merchant METHOD — the COD switch (native fee via the integrations `cod`
|
|
27
|
+
* block) or a manual method (no fee) — the orchestration hook resolves
|
|
28
|
+
* them (`hasCard`/`hasCod`/`cardId`/`codMethodId`); both listings arrive
|
|
29
|
+
* rule-filtered server-side (`checkout.listPaymentProviders`,
|
|
30
|
+
* docs/storefront/checkout.md). The method tab stays a separate rail —
|
|
31
|
+
* it's a provider-less session, not a Stripe payment method.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
type CheckoutPaymentMethodListProps = {
|
|
@@ -87,6 +88,15 @@ type CheckoutPaymentMethodListProps = {
|
|
|
87
88
|
total?: number
|
|
88
89
|
/** Operational-visibility sink (successor of @1click's logCheckoutError). */
|
|
89
90
|
logError?: CheckoutLogError
|
|
91
|
+
/**
|
|
92
|
+
* Additional payment tabs rendered as radio cards in the same visual
|
|
93
|
+
* language as the built-in card/cod tabs. The seam for embedded
|
|
94
|
+
* third-party providers (a myPOS-style frame carries its own pay
|
|
95
|
+
* action, so while an extra tab is active the PaymentButton is hidden
|
|
96
|
+
* and the tab's `content` owns completion). Absent = exact previous
|
|
97
|
+
* behavior.
|
|
98
|
+
*/
|
|
99
|
+
extraTabs?: Array<{ id: string; label: string; content: React.ReactNode }>
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export function CheckoutPaymentMethodList({
|
|
@@ -105,9 +115,14 @@ export function CheckoutPaymentMethodList({
|
|
|
105
115
|
beforePaymentButton,
|
|
106
116
|
total,
|
|
107
117
|
logError,
|
|
118
|
+
extraTabs,
|
|
108
119
|
}: CheckoutPaymentMethodListProps) {
|
|
109
120
|
const labels = useCheckoutLabels()
|
|
110
121
|
|
|
122
|
+
// Which extra tab is active, if any. Selecting card/cod clears it; the
|
|
123
|
+
// radio group stays single-select across built-in and extra tabs.
|
|
124
|
+
const [extraTab, setExtraTab] = useState<string | null>(null)
|
|
125
|
+
|
|
111
126
|
// When the store opts out of gating (gatePaymentUntilDelivery=false),
|
|
112
127
|
// the payment section is always shown + interactive regardless of
|
|
113
128
|
// delivery readiness. The Buy button remains independently gated by
|
|
@@ -203,23 +218,26 @@ export function CheckoutPaymentMethodList({
|
|
|
203
218
|
<div
|
|
204
219
|
className={cn(
|
|
205
220
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
206
|
-
paymentTab === "card"
|
|
221
|
+
paymentTab === "card" && !extraTab
|
|
207
222
|
? "border-primary bg-primary/5"
|
|
208
223
|
: "border-border bg-card hover:border-muted-foreground"
|
|
209
224
|
)}
|
|
210
225
|
>
|
|
211
226
|
<button
|
|
212
227
|
type="button"
|
|
213
|
-
onClick={() =>
|
|
228
|
+
onClick={() => {
|
|
229
|
+
setExtraTab(null)
|
|
230
|
+
onPaymentTab("card")
|
|
231
|
+
}}
|
|
214
232
|
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
215
233
|
>
|
|
216
234
|
<div
|
|
217
235
|
className={cn(
|
|
218
236
|
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
219
|
-
paymentTab === "card" ? "border-primary" : "border-border"
|
|
237
|
+
paymentTab === "card" && !extraTab ? "border-primary" : "border-border"
|
|
220
238
|
)}
|
|
221
239
|
>
|
|
222
|
-
{paymentTab === "card" && (
|
|
240
|
+
{paymentTab === "card" && !extraTab && (
|
|
223
241
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
224
242
|
)}
|
|
225
243
|
</div>
|
|
@@ -228,7 +246,7 @@ export function CheckoutPaymentMethodList({
|
|
|
228
246
|
</span>
|
|
229
247
|
</button>
|
|
230
248
|
|
|
231
|
-
{paymentTab === "card" && (
|
|
249
|
+
{paymentTab === "card" && !extraTab && (
|
|
232
250
|
<div className="px-4 pb-4 pt-2">
|
|
233
251
|
{/*
|
|
234
252
|
Skeleton vs PaymentElement gated by stripeReady.
|
|
@@ -279,23 +297,26 @@ export function CheckoutPaymentMethodList({
|
|
|
279
297
|
<div
|
|
280
298
|
className={cn(
|
|
281
299
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
282
|
-
paymentTab === "cod"
|
|
300
|
+
paymentTab === "cod" && !extraTab
|
|
283
301
|
? "border-primary bg-primary/5"
|
|
284
302
|
: "border-border bg-card hover:border-muted-foreground"
|
|
285
303
|
)}
|
|
286
304
|
>
|
|
287
305
|
<button
|
|
288
306
|
type="button"
|
|
289
|
-
onClick={() =>
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setExtraTab(null)
|
|
309
|
+
onPaymentTab("cod")
|
|
310
|
+
}}
|
|
290
311
|
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
291
312
|
>
|
|
292
313
|
<div
|
|
293
314
|
className={cn(
|
|
294
315
|
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
295
|
-
paymentTab === "cod" ? "border-primary" : "border-border"
|
|
316
|
+
paymentTab === "cod" && !extraTab ? "border-primary" : "border-border"
|
|
296
317
|
)}
|
|
297
318
|
>
|
|
298
|
-
{paymentTab === "cod" && (
|
|
319
|
+
{paymentTab === "cod" && !extraTab && (
|
|
299
320
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
300
321
|
)}
|
|
301
322
|
</div>
|
|
@@ -303,7 +324,7 @@ export function CheckoutPaymentMethodList({
|
|
|
303
324
|
{labels.cashOnDelivery}
|
|
304
325
|
</span>
|
|
305
326
|
</button>
|
|
306
|
-
{paymentTab === "cod" && (
|
|
327
|
+
{paymentTab === "cod" && !extraTab && (
|
|
307
328
|
<div className="px-4 pb-3 pt-0">
|
|
308
329
|
<p className="text-xs text-muted-foreground ml-[30px]">
|
|
309
330
|
{labels.codNote}
|
|
@@ -312,6 +333,41 @@ export function CheckoutPaymentMethodList({
|
|
|
312
333
|
)}
|
|
313
334
|
</div>
|
|
314
335
|
)}
|
|
336
|
+
|
|
337
|
+
{extraTabs?.map((tab) => (
|
|
338
|
+
<div
|
|
339
|
+
key={tab.id}
|
|
340
|
+
className={cn(
|
|
341
|
+
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
342
|
+
extraTab === tab.id
|
|
343
|
+
? "border-primary bg-primary/5"
|
|
344
|
+
: "border-border bg-card hover:border-muted-foreground"
|
|
345
|
+
)}
|
|
346
|
+
>
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
onClick={() => setExtraTab(tab.id)}
|
|
350
|
+
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
351
|
+
>
|
|
352
|
+
<div
|
|
353
|
+
className={cn(
|
|
354
|
+
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
355
|
+
extraTab === tab.id ? "border-primary" : "border-border"
|
|
356
|
+
)}
|
|
357
|
+
>
|
|
358
|
+
{extraTab === tab.id && (
|
|
359
|
+
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<span className="text-sm font-medium text-foreground">
|
|
363
|
+
{tab.label}
|
|
364
|
+
</span>
|
|
365
|
+
</button>
|
|
366
|
+
{extraTab === tab.id && (
|
|
367
|
+
<div className="px-4 pb-4 pt-2">{tab.content}</div>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
315
371
|
</div>
|
|
316
372
|
|
|
317
373
|
<ErrorMessage
|
|
@@ -323,7 +379,7 @@ export function CheckoutPaymentMethodList({
|
|
|
323
379
|
<div className="mt-5">{beforePaymentButton}</div>
|
|
324
380
|
)}
|
|
325
381
|
|
|
326
|
-
{buyButtonNotReady && buyButtonNotReadyReason && (
|
|
382
|
+
{!extraTab && buyButtonNotReady && buyButtonNotReadyReason && (
|
|
327
383
|
<div className="mt-4 flex items-center justify-center gap-1.5 text-muted-foreground">
|
|
328
384
|
<svg
|
|
329
385
|
viewBox="0 0 24 24"
|
|
@@ -344,20 +400,24 @@ export function CheckoutPaymentMethodList({
|
|
|
344
400
|
</div>
|
|
345
401
|
)}
|
|
346
402
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
{/* An active extra tab owns its own pay action (embedded
|
|
404
|
+
provider frames ship their own button), so ours hides. */}
|
|
405
|
+
{!extraTab && (
|
|
406
|
+
<div className="mt-5">
|
|
407
|
+
<PaymentButton
|
|
408
|
+
cart={cart}
|
|
409
|
+
paymentTab={paymentTab}
|
|
410
|
+
notReady={buyButtonNotReady}
|
|
411
|
+
performBuyClick={performBuyClick}
|
|
412
|
+
paymentElementComplete={
|
|
413
|
+
paymentTab === "cod" ? true : paymentElementComplete
|
|
414
|
+
}
|
|
415
|
+
total={total}
|
|
416
|
+
logError={logError}
|
|
417
|
+
data-testid="submit-order-button"
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
361
421
|
</StripeElementsScope>
|
|
362
422
|
)}
|
|
363
423
|
</div>
|