@cartbase/storefront 0.20.1 → 0.22.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 +274 -258
- package/src/api/checkout.ts +15 -0
- package/src/api/http.ts +10 -0
- package/src/api/integrations.ts +121 -0
- package/src/api/store.ts +21 -0
- package/src/cart-drawer/payment-badges.tsx +36 -96
- package/src/checkout/card-offer.ts +68 -0
- package/src/checkout/carrier-marks.ts +53 -0
- package/src/checkout/checkout-client.tsx +71 -13
- package/src/checkout/checkout-error-screen.tsx +113 -0
- package/src/checkout/discount-section.tsx +88 -56
- package/src/checkout/fulfillment-option.ts +30 -0
- package/src/checkout/index.ts +41 -6
- package/src/checkout/labels.ts +98 -0
- package/src/checkout/line-item-card.tsx +35 -83
- package/src/checkout/mobile-checkout-bottom-bar.tsx +132 -0
- package/src/checkout/mobile-checkout-top-bar.tsx +94 -0
- package/src/checkout/mobile-order-summary-body.tsx +172 -0
- package/src/checkout/order-summary.tsx +85 -179
- package/src/checkout/payment-button.tsx +25 -8
- package/src/checkout/payment-method-list.tsx +51 -13
- package/src/checkout/payment-wrapper.tsx +57 -13
- package/src/checkout/pickup-option.ts +35 -0
- package/src/checkout/pickup-point-selector.tsx +372 -0
- package/src/checkout/pigeon-office-selector.tsx +379 -0
- package/src/checkout/shipping-method-list.tsx +102 -10
- package/src/checkout/summary-math.ts +152 -0
- package/src/checkout/use-checkout-funnel.ts +303 -0
- package/src/checkout/use-checkout-orchestration.ts +384 -30
- package/src/common/icons/cartbase-mark.ts +9 -0
- package/src/common/icons/payment-marks.ts +73 -0
- package/src/common/icons/social-marks.ts +61 -0
- package/src/common/index.ts +15 -0
- package/src/common/payment-icons.tsx +54 -0
- package/src/common/powered-by-cartbase.tsx +47 -0
- package/src/common/social-links.tsx +65 -0
- package/src/lib/stripe-env.ts +25 -0
- package/src/locales/bg.ts +39 -0
- package/src/locales/es.ts +37 -0
- package/src/store/labels.ts +10 -0
- package/src/tracking/attribution.ts +83 -0
- package/src/tracking/consent.ts +53 -0
- package/src/tracking/events.ts +113 -0
- package/src/tracking/fbq.ts +48 -0
- package/src/tracking/gtag.ts +32 -0
- package/src/tracking/index.ts +32 -1
- package/src/tracking/once.ts +137 -0
- package/src/tracking/rybbit-events.ts +42 -0
- package/src/tracking/ttq.ts +24 -0
- package/src/tracking/types.ts +34 -0
|
@@ -1,76 +1,44 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
|
|
5
3
|
import type { StorefrontClient } from "../api/http"
|
|
6
|
-
import {
|
|
4
|
+
import type { Cart, CartLineItem } from "../api/carts"
|
|
7
5
|
import { Price } from "../lib/price"
|
|
8
|
-
import { cn } from "../lib/utils"
|
|
9
6
|
import { variantCaption } from "../lib/variant-caption"
|
|
10
7
|
import { useCheckoutLabels } from "./context"
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
10
|
* LineItemCard — a single cart line shown inside the checkout order
|
|
14
|
-
* summary card
|
|
15
|
-
* and price.
|
|
11
|
+
* summary card: thumbnail, title, variant, how many, price.
|
|
16
12
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `onCartChange` so the host can update its cart state (or
|
|
23
|
-
* `router.refresh()` in an RSC app). `variant.title` → the flat
|
|
24
|
-
* `variant_title` column of the Cartbase line DTO.
|
|
13
|
+
* **A CHECKOUT IS A CHECKOUT** (Alexander, 2026-09-18). This card carried a
|
|
14
|
+
* quantity select, ported from the platform this kit came from, and it is
|
|
15
|
+
* gone with the summary row's stepper: the cart is changed in the cart, and
|
|
16
|
+
* the checkout states the order. Nothing in the checkout family writes a
|
|
17
|
+
* line any more.
|
|
25
18
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
19
|
+
* Ported from `@1click/ui/src/checkout/line-item-card.tsx` (v2.3.1);
|
|
20
|
+
* `variant.title` → the flat `variant_title` column of the Cartbase line
|
|
21
|
+
* DTO.
|
|
28
22
|
*/
|
|
29
23
|
|
|
30
24
|
type LineItemCardProps = {
|
|
31
|
-
/** The SDK transport — quantity changes go through it. */
|
|
32
|
-
client: StorefrontClient
|
|
33
|
-
cartId: string
|
|
34
25
|
item: CartLineItem
|
|
35
26
|
currencyCode: string
|
|
36
|
-
/**
|
|
27
|
+
/** @deprecated Unused: the checkout summary no longer writes. Accepted so
|
|
28
|
+
* a store passing the old props keeps compiling; remove them at leisure. */
|
|
29
|
+
client?: StorefrontClient
|
|
30
|
+
/** @deprecated Unused — see `client`. */
|
|
31
|
+
cartId?: string
|
|
32
|
+
/** @deprecated Unused — see `client`. */
|
|
37
33
|
onCartChange?: (cart: Cart) => void
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
export function LineItemCard({
|
|
41
|
-
client,
|
|
42
|
-
cartId,
|
|
43
|
-
item,
|
|
44
|
-
currencyCode,
|
|
45
|
-
onCartChange,
|
|
46
|
-
}: LineItemCardProps) {
|
|
36
|
+
export function LineItemCard({ item, currencyCode }: LineItemCardProps) {
|
|
47
37
|
const labels = useCheckoutLabels()
|
|
48
|
-
const [updating, setUpdating] = useState(false)
|
|
49
|
-
|
|
50
|
-
const handleQty = async (qty: number) => {
|
|
51
|
-
setUpdating(true)
|
|
52
|
-
try {
|
|
53
|
-
const { cart } = await updateLineItem(client, cartId, item.id, {
|
|
54
|
-
quantity: qty,
|
|
55
|
-
})
|
|
56
|
-
onCartChange?.(cart)
|
|
57
|
-
} catch {
|
|
58
|
-
// Quantity update failed (e.g. insufficient_inventory) — the select
|
|
59
|
-
// snaps back to the server value on the next cart render.
|
|
60
|
-
} finally {
|
|
61
|
-
setUpdating(false)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
38
|
const total = item.total ?? 0
|
|
66
39
|
|
|
67
40
|
return (
|
|
68
|
-
<div
|
|
69
|
-
className={cn(
|
|
70
|
-
"relative flex gap-4 p-4 rounded-xl border border-border bg-muted/50 transition-opacity",
|
|
71
|
-
updating && "opacity-40 pointer-events-none"
|
|
72
|
-
)}
|
|
73
|
-
>
|
|
41
|
+
<div className="relative flex gap-4 p-4 rounded-xl border border-border bg-muted/50">
|
|
74
42
|
<div className="relative w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 bg-card border border-border">
|
|
75
43
|
{item.thumbnail ? (
|
|
76
44
|
// eslint-disable-next-line @next/next/no-img-element
|
|
@@ -113,42 +81,26 @@ export function LineItemCard({
|
|
|
113
81
|
</p>
|
|
114
82
|
)}
|
|
115
83
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
className="h-8 pl-2.5 pr-7 text-xs font-medium border border-border rounded-lg bg-card text-foreground appearance-none cursor-pointer hover:border-muted-foreground transition-colors focus:outline-none focus:ring-1 focus:ring-primary/20"
|
|
122
|
-
>
|
|
123
|
-
{Array.from({ length: 10 }, (_, i) => (
|
|
124
|
-
<option key={i + 1} value={i + 1}>
|
|
125
|
-
{labels.qty} {i + 1}
|
|
126
|
-
</option>
|
|
127
|
-
))}
|
|
128
|
-
</select>
|
|
129
|
-
<svg
|
|
130
|
-
className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none"
|
|
131
|
-
fill="none"
|
|
132
|
-
viewBox="0 0 24 24"
|
|
133
|
-
stroke="currentColor"
|
|
134
|
-
strokeWidth={2}
|
|
135
|
-
>
|
|
136
|
-
<path
|
|
137
|
-
strokeLinecap="round"
|
|
138
|
-
strokeLinejoin="round"
|
|
139
|
-
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
140
|
-
/>
|
|
141
|
-
</svg>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
84
|
+
{/* How many, said and not offered: the quantity select that stood
|
|
85
|
+
here could change the order from inside the checkout. */}
|
|
86
|
+
<p className="text-xs font-medium text-muted-foreground mt-2.5 tabular-nums">
|
|
87
|
+
{labels.qty} {item.quantity}
|
|
88
|
+
</p>
|
|
144
89
|
</div>
|
|
145
90
|
|
|
146
91
|
<div className="flex flex-col items-end justify-center flex-shrink-0">
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
92
|
+
{/* A line that costs nothing says so; €0.00 reads as a fault. */}
|
|
93
|
+
{total <= 0 ? (
|
|
94
|
+
<span className="text-xs font-bold text-success uppercase">
|
|
95
|
+
{labels.lineFree}
|
|
96
|
+
</span>
|
|
97
|
+
) : (
|
|
98
|
+
<Price
|
|
99
|
+
amount={total}
|
|
100
|
+
currencyCode={currencyCode}
|
|
101
|
+
className="text-sm font-bold text-foreground"
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
152
104
|
</div>
|
|
153
105
|
</div>
|
|
154
106
|
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import type { StorefrontClient } from "../api/http"
|
|
6
|
+
import type { Cart } from "../api/carts"
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import { useCheckoutLabels } from "./context"
|
|
10
|
+
import { MobileOrderSummaryBody } from "./mobile-order-summary-body"
|
|
11
|
+
import { checkoutTotals } from "./summary-math"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MobileCheckoutBottomBar — the card that sits directly above Place order
|
|
15
|
+
* on a phone: the first item's picture with the number of units on it, the
|
|
16
|
+
* word Total over the count, the figure, and a chevron onto the same body
|
|
17
|
+
* the top bar opens. It is the last thing a shopper reads before paying,
|
|
18
|
+
* so what they are paying for is one tap away at the moment it matters.
|
|
19
|
+
*
|
|
20
|
+
* It opens upwards, the body above its own row, so the row and the Buy
|
|
21
|
+
* button under it stay where the thumb already is. Hidden from the small
|
|
22
|
+
* breakpoint up, where the summary column is always open.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type MobileCheckoutBottomBarProps = {
|
|
26
|
+
/** The SDK transport, for the doors inside the opened body. */
|
|
27
|
+
client: StorefrontClient
|
|
28
|
+
cart: Cart
|
|
29
|
+
optimisticShippingCost: number | null
|
|
30
|
+
optimisticMethodFee?: number | null
|
|
31
|
+
methodFeeLabel?: string
|
|
32
|
+
showGiftCards?: boolean
|
|
33
|
+
/** Hide the promo door inside the body; see MobileOrderSummaryBody. */
|
|
34
|
+
hideDiscount?: boolean
|
|
35
|
+
onCartChange?: (cart: Cart) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function MobileCheckoutBottomBar({
|
|
39
|
+
client,
|
|
40
|
+
cart,
|
|
41
|
+
optimisticShippingCost,
|
|
42
|
+
optimisticMethodFee,
|
|
43
|
+
methodFeeLabel,
|
|
44
|
+
showGiftCards,
|
|
45
|
+
hideDiscount,
|
|
46
|
+
onCartChange,
|
|
47
|
+
}: MobileCheckoutBottomBarProps) {
|
|
48
|
+
const labels = useCheckoutLabels()
|
|
49
|
+
const [open, setOpen] = useState(false)
|
|
50
|
+
const { displayTotal, itemCount, productItems } = checkoutTotals({
|
|
51
|
+
cart,
|
|
52
|
+
optimisticShippingCost,
|
|
53
|
+
optimisticMethodFee,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const firstItem = productItems[0]
|
|
57
|
+
if (!firstItem) return null
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="sm:hidden rounded-[2px] border border-border bg-card overflow-hidden">
|
|
61
|
+
{open && (
|
|
62
|
+
<div className="px-5 pt-5 pb-2">
|
|
63
|
+
<MobileOrderSummaryBody
|
|
64
|
+
client={client}
|
|
65
|
+
cart={cart}
|
|
66
|
+
optimisticShippingCost={optimisticShippingCost}
|
|
67
|
+
optimisticMethodFee={optimisticMethodFee}
|
|
68
|
+
methodFeeLabel={methodFeeLabel}
|
|
69
|
+
showGiftCards={showGiftCards}
|
|
70
|
+
hideDiscount={hideDiscount}
|
|
71
|
+
onCartChange={onCartChange}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => setOpen((v) => !v)}
|
|
79
|
+
className={cn(
|
|
80
|
+
"w-full flex items-center gap-3 px-4 py-3 text-left",
|
|
81
|
+
open && "border-t border-border"
|
|
82
|
+
)}
|
|
83
|
+
aria-expanded={open}
|
|
84
|
+
>
|
|
85
|
+
<div className="relative flex-shrink-0">
|
|
86
|
+
<div className="w-12 h-12 rounded-lg overflow-hidden bg-muted border border-border">
|
|
87
|
+
{firstItem.thumbnail ? (
|
|
88
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
89
|
+
<img
|
|
90
|
+
src={firstItem.thumbnail}
|
|
91
|
+
alt={firstItem.product_title ?? ""}
|
|
92
|
+
className="w-full h-full object-cover"
|
|
93
|
+
/>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
{itemCount > 1 && (
|
|
97
|
+
<span className="absolute -top-1.5 -right-1.5 min-w-[20px] h-[20px] px-1 text-[10px] font-bold rounded-full bg-foreground text-card flex items-center justify-center shadow-sm ring-2 ring-card">
|
|
98
|
+
{itemCount}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex-1 min-w-0">
|
|
104
|
+
<p className="text-base font-bold text-foreground leading-tight">{labels.total}</p>
|
|
105
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
106
|
+
{itemCount} {labels.items}
|
|
107
|
+
</p>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center gap-1.5">
|
|
111
|
+
<Price
|
|
112
|
+
amount={displayTotal}
|
|
113
|
+
currencyCode={cart.currency_code}
|
|
114
|
+
className="text-base font-bold text-foreground tracking-tight"
|
|
115
|
+
/>
|
|
116
|
+
<svg
|
|
117
|
+
className={cn(
|
|
118
|
+
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
119
|
+
open && "rotate-180"
|
|
120
|
+
)}
|
|
121
|
+
fill="none"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
stroke="currentColor"
|
|
124
|
+
strokeWidth={2.25}
|
|
125
|
+
>
|
|
126
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
127
|
+
</svg>
|
|
128
|
+
</div>
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import type { StorefrontClient } from "../api/http"
|
|
6
|
+
import type { Cart } from "../api/carts"
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import { useCheckoutLabels } from "./context"
|
|
10
|
+
import { MobileOrderSummaryBody } from "./mobile-order-summary-body"
|
|
11
|
+
import { checkoutTotals } from "./summary-math"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MobileCheckoutTopBar — the strip a phone shopper meets first, above the
|
|
15
|
+
* contact form: the words "Order summary", a chevron, and the total. It
|
|
16
|
+
* opens onto the items and the breakdown.
|
|
17
|
+
*
|
|
18
|
+
* Closed by default, because the first thing a shopper checks is the
|
|
19
|
+
* figure, and only then, if they want, what makes it up. The desktop
|
|
20
|
+
* column is always open, so this bar is hidden from the small breakpoint
|
|
21
|
+
* up: the two never show at once.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type MobileCheckoutTopBarProps = {
|
|
25
|
+
/** The SDK transport, for the doors inside the opened body. */
|
|
26
|
+
client: StorefrontClient
|
|
27
|
+
cart: Cart
|
|
28
|
+
optimisticShippingCost: number | null
|
|
29
|
+
optimisticMethodFee?: number | null
|
|
30
|
+
methodFeeLabel?: string
|
|
31
|
+
showGiftCards?: boolean
|
|
32
|
+
onCartChange?: (cart: Cart) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MobileCheckoutTopBar({
|
|
36
|
+
client,
|
|
37
|
+
cart,
|
|
38
|
+
optimisticShippingCost,
|
|
39
|
+
optimisticMethodFee,
|
|
40
|
+
methodFeeLabel,
|
|
41
|
+
showGiftCards,
|
|
42
|
+
onCartChange,
|
|
43
|
+
}: MobileCheckoutTopBarProps) {
|
|
44
|
+
const labels = useCheckoutLabels()
|
|
45
|
+
const [open, setOpen] = useState(false)
|
|
46
|
+
const { displayTotal } = checkoutTotals({
|
|
47
|
+
cart,
|
|
48
|
+
optimisticShippingCost,
|
|
49
|
+
optimisticMethodFee,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="sm:hidden bg-muted border-y border-border">
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => setOpen((v) => !v)}
|
|
57
|
+
className="w-full flex items-center justify-between px-5 py-3.5 text-left"
|
|
58
|
+
aria-expanded={open}
|
|
59
|
+
>
|
|
60
|
+
<span className="inline-flex items-center gap-1.5 text-sm font-medium text-primary">
|
|
61
|
+
{labels.orderSummary}
|
|
62
|
+
<svg
|
|
63
|
+
className={cn("w-4 h-4 transition-transform duration-200", open && "rotate-180")}
|
|
64
|
+
fill="none"
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
strokeWidth={2.25}
|
|
68
|
+
>
|
|
69
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
|
70
|
+
</svg>
|
|
71
|
+
</span>
|
|
72
|
+
<Price
|
|
73
|
+
amount={displayTotal}
|
|
74
|
+
currencyCode={cart.currency_code}
|
|
75
|
+
className="text-base font-bold text-foreground tracking-tight"
|
|
76
|
+
/>
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
{open && (
|
|
80
|
+
<div className="px-5 pb-5">
|
|
81
|
+
<MobileOrderSummaryBody
|
|
82
|
+
client={client}
|
|
83
|
+
cart={cart}
|
|
84
|
+
optimisticShippingCost={optimisticShippingCost}
|
|
85
|
+
optimisticMethodFee={optimisticMethodFee}
|
|
86
|
+
methodFeeLabel={methodFeeLabel}
|
|
87
|
+
showGiftCards={showGiftCards}
|
|
88
|
+
onCartChange={onCartChange}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import type { StorefrontClient } from "../api/http"
|
|
4
|
+
import type { Cart } from "../api/carts"
|
|
5
|
+
import { Price } from "../lib/price"
|
|
6
|
+
import { useCheckoutLabels } from "./context"
|
|
7
|
+
import { DiscountSection } from "./discount-section"
|
|
8
|
+
import { GiftCardSection } from "./gift-card-section"
|
|
9
|
+
import { CheckoutLineItem } from "./order-summary"
|
|
10
|
+
import { checkoutTotals, isCompanyOrder } from "./summary-math"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MobileOrderSummaryBody — what the two mobile summary bars reveal when a
|
|
14
|
+
* shopper opens them: the items with their counts, the discount and
|
|
15
|
+
* gift-card doors, and the same breakdown the desktop column prints.
|
|
16
|
+
*
|
|
17
|
+
* It carries no card of its own: the top bar is a utility strip and the
|
|
18
|
+
* bottom bar is a product card, so each parent decides its own ground. The
|
|
19
|
+
* grand total is not repeated here either, because the bar's own row shows
|
|
20
|
+
* it whether the body is open or closed.
|
|
21
|
+
*
|
|
22
|
+
* The numbers come from `checkoutTotals`, the same function the desktop
|
|
23
|
+
* summary calls, so the three surfaces cannot drift apart.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
type MobileOrderSummaryBodyProps = {
|
|
27
|
+
/** The SDK transport — the promo and gift-card doors run through it. */
|
|
28
|
+
client: StorefrontClient
|
|
29
|
+
cart: Cart
|
|
30
|
+
optimisticShippingCost: number | null
|
|
31
|
+
optimisticMethodFee?: number | null
|
|
32
|
+
/** Override for the fee row's name; the cart's own label otherwise. */
|
|
33
|
+
methodFeeLabel?: string
|
|
34
|
+
/** Show the gift-card door. Default true, as on the desktop summary. */
|
|
35
|
+
showGiftCards?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* Hide the promo door inside this body. A layout that surfaces an
|
|
38
|
+
* always-open discount field of its own (the checkout does this on the
|
|
39
|
+
* phone, right above the bottom bar) passes true so the shopper is not
|
|
40
|
+
* offered the same thing twice.
|
|
41
|
+
*/
|
|
42
|
+
hideDiscount?: boolean
|
|
43
|
+
onCartChange?: (cart: Cart) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function MobileOrderSummaryBody({
|
|
47
|
+
client,
|
|
48
|
+
cart,
|
|
49
|
+
optimisticShippingCost,
|
|
50
|
+
optimisticMethodFee,
|
|
51
|
+
methodFeeLabel,
|
|
52
|
+
showGiftCards = true,
|
|
53
|
+
hideDiscount = false,
|
|
54
|
+
onCartChange,
|
|
55
|
+
}: MobileOrderSummaryBodyProps) {
|
|
56
|
+
const labels = useCheckoutLabels()
|
|
57
|
+
const t = checkoutTotals({ cart, optimisticShippingCost, optimisticMethodFee })
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-4 pt-4">
|
|
61
|
+
<div className="divide-y divide-border">
|
|
62
|
+
{t.productItems.map((item) => (
|
|
63
|
+
<div key={item.id} className="py-5 first:pt-0">
|
|
64
|
+
<CheckoutLineItem
|
|
65
|
+
item={item}
|
|
66
|
+
currencyCode={cart.currency_code}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{(!hideDiscount || showGiftCards) && (
|
|
73
|
+
<div className="space-y-3">
|
|
74
|
+
{!hideDiscount && (
|
|
75
|
+
<DiscountSection client={client} cart={cart} onCartChange={onCartChange} />
|
|
76
|
+
)}
|
|
77
|
+
{showGiftCards && (
|
|
78
|
+
<GiftCardSection client={client} cart={cart} onCartChange={onCartChange} />
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<div className="space-y-2 pt-2">
|
|
84
|
+
<div className="flex justify-between text-sm">
|
|
85
|
+
<span className="text-muted-foreground">
|
|
86
|
+
{labels.subtotal} · {t.itemCount} {labels.items}
|
|
87
|
+
</span>
|
|
88
|
+
<Price
|
|
89
|
+
amount={t.productSubtotal}
|
|
90
|
+
currencyCode={cart.currency_code}
|
|
91
|
+
className="font-medium text-foreground"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="flex justify-between text-sm">
|
|
96
|
+
<span className="text-muted-foreground">{labels.shipping}</span>
|
|
97
|
+
{t.shippingKnown ? (
|
|
98
|
+
t.shippingCost === 0 ? (
|
|
99
|
+
<span className="font-medium text-success">{labels.shippingFree}</span>
|
|
100
|
+
) : (
|
|
101
|
+
<Price
|
|
102
|
+
amount={t.shippingCost ?? 0}
|
|
103
|
+
currencyCode={cart.currency_code}
|
|
104
|
+
className="font-medium text-foreground"
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
) : (
|
|
108
|
+
<span className="font-medium text-foreground">{labels.shippingCalc}</span>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{t.methodFeeAmount > 0 && (
|
|
113
|
+
<div className="flex justify-between text-sm">
|
|
114
|
+
<span className="text-muted-foreground">
|
|
115
|
+
{methodFeeLabel ?? cart.payment_method_fee_label ?? labels.paymentMethodFee}
|
|
116
|
+
</span>
|
|
117
|
+
<Price
|
|
118
|
+
amount={t.methodFeeAmount}
|
|
119
|
+
currencyCode={cart.currency_code}
|
|
120
|
+
className="font-medium text-foreground"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{!!cart.discount_total && (
|
|
126
|
+
<div className="flex justify-between text-sm text-success">
|
|
127
|
+
<span>{labels.discount}</span>
|
|
128
|
+
<span className="font-medium">
|
|
129
|
+
-
|
|
130
|
+
<Price amount={cart.discount_total} currencyCode={cart.currency_code} />
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
|
|
135
|
+
{/* For a company, and only when the figure is known: a consumer
|
|
136
|
+
sees the price they pay, VAT included (see OrderSummary). */}
|
|
137
|
+
{isCompanyOrder(cart) && (cart.tax_total ?? 0) > 0 && (
|
|
138
|
+
<div className="flex justify-between text-sm">
|
|
139
|
+
<span className="text-muted-foreground">{labels.tax}</span>
|
|
140
|
+
<Price
|
|
141
|
+
amount={cart.tax_total ?? 0}
|
|
142
|
+
currencyCode={cart.currency_code}
|
|
143
|
+
className="font-medium text-foreground"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{/* Tender, not a reduction: the gift card covers part of the total
|
|
149
|
+
the bar shows, it does not lower it (docs/storefront/gift-cards.md). */}
|
|
150
|
+
{t.giftCardTotal > 0 && (
|
|
151
|
+
<>
|
|
152
|
+
<div className="flex justify-between text-sm text-success">
|
|
153
|
+
<span>{labels.giftCardApplied}</span>
|
|
154
|
+
<span className="font-medium">
|
|
155
|
+
-
|
|
156
|
+
<Price amount={t.giftCardTotal} currencyCode={cart.currency_code} />
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex justify-between text-sm">
|
|
160
|
+
<span className="text-muted-foreground">{labels.giftCardRemaining}</span>
|
|
161
|
+
<Price
|
|
162
|
+
amount={t.giftCardRemainder}
|
|
163
|
+
currencyCode={cart.currency_code}
|
|
164
|
+
className="font-semibold text-foreground"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|