@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.
Files changed (50) hide show
  1. package/package.json +274 -258
  2. package/src/api/checkout.ts +15 -0
  3. package/src/api/http.ts +10 -0
  4. package/src/api/integrations.ts +121 -0
  5. package/src/api/store.ts +21 -0
  6. package/src/cart-drawer/payment-badges.tsx +36 -96
  7. package/src/checkout/card-offer.ts +68 -0
  8. package/src/checkout/carrier-marks.ts +53 -0
  9. package/src/checkout/checkout-client.tsx +71 -13
  10. package/src/checkout/checkout-error-screen.tsx +113 -0
  11. package/src/checkout/discount-section.tsx +88 -56
  12. package/src/checkout/fulfillment-option.ts +30 -0
  13. package/src/checkout/index.ts +41 -6
  14. package/src/checkout/labels.ts +98 -0
  15. package/src/checkout/line-item-card.tsx +35 -83
  16. package/src/checkout/mobile-checkout-bottom-bar.tsx +132 -0
  17. package/src/checkout/mobile-checkout-top-bar.tsx +94 -0
  18. package/src/checkout/mobile-order-summary-body.tsx +172 -0
  19. package/src/checkout/order-summary.tsx +85 -179
  20. package/src/checkout/payment-button.tsx +25 -8
  21. package/src/checkout/payment-method-list.tsx +51 -13
  22. package/src/checkout/payment-wrapper.tsx +57 -13
  23. package/src/checkout/pickup-option.ts +35 -0
  24. package/src/checkout/pickup-point-selector.tsx +372 -0
  25. package/src/checkout/pigeon-office-selector.tsx +379 -0
  26. package/src/checkout/shipping-method-list.tsx +102 -10
  27. package/src/checkout/summary-math.ts +152 -0
  28. package/src/checkout/use-checkout-funnel.ts +303 -0
  29. package/src/checkout/use-checkout-orchestration.ts +384 -30
  30. package/src/common/icons/cartbase-mark.ts +9 -0
  31. package/src/common/icons/payment-marks.ts +73 -0
  32. package/src/common/icons/social-marks.ts +61 -0
  33. package/src/common/index.ts +15 -0
  34. package/src/common/payment-icons.tsx +54 -0
  35. package/src/common/powered-by-cartbase.tsx +47 -0
  36. package/src/common/social-links.tsx +65 -0
  37. package/src/lib/stripe-env.ts +25 -0
  38. package/src/locales/bg.ts +39 -0
  39. package/src/locales/es.ts +37 -0
  40. package/src/store/labels.ts +10 -0
  41. package/src/tracking/attribution.ts +83 -0
  42. package/src/tracking/consent.ts +53 -0
  43. package/src/tracking/events.ts +113 -0
  44. package/src/tracking/fbq.ts +48 -0
  45. package/src/tracking/gtag.ts +32 -0
  46. package/src/tracking/index.ts +32 -1
  47. package/src/tracking/once.ts +137 -0
  48. package/src/tracking/rybbit-events.ts +42 -0
  49. package/src/tracking/ttq.ts +24 -0
  50. 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 { updateLineItem, type Cart, type CartLineItem } from "../api/carts"
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. Includes thumbnail, title, variant, quantity select,
15
- * and price.
11
+ * summary card: thumbnail, title, variant, how many, price.
16
12
  *
17
- * Ported from `@1click/ui/src/checkout/line-item-card.tsx` (v2.3.1) with
18
- * the Cartbase data seam: the cookie-bound `updateLineItem` server action
19
- * `carts.updateLineItem(client, cartId, lineId, {quantity})`
20
- * (POST /api/store/carts/:id/line-items/:lineId docs/storefront/
21
- * carts.md). The route returns the freshly decorated cart; pass
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
- * Checkout is master-level: customers cannot remove items here.
27
- * Removal lives in the cart drawer only.
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
- /** Receives the decorated cart returned by the quantity update. */
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
- <div className="flex items-center gap-2 mt-2.5">
117
- <div className="relative">
118
- <select
119
- value={item.quantity}
120
- onChange={(e) => handleQty(parseInt(e.target.value, 10))}
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
- <Price
148
- amount={total}
149
- currencyCode={currencyCode}
150
- className="text-sm font-bold text-foreground"
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
+ }