@cartbase/storefront 0.20.0 → 0.20.1
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/LICENSE +21 -21
- package/package.json +258 -258
- package/src/api/auth.ts +108 -108
- package/src/api/carts.ts +523 -523
- package/src/api/categories.ts +184 -184
- package/src/api/checkout.ts +526 -526
- package/src/api/collections.ts +130 -130
- package/src/api/consent.ts +75 -75
- package/src/api/content.ts +125 -125
- package/src/api/customers.ts +303 -303
- package/src/api/gift-cards.ts +112 -112
- package/src/api/http.ts +180 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +35 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/index.ts +20 -20
- package/src/common/market-select.tsx +57 -57
- package/src/index.ts +12 -12
- package/src/lib/country-name.ts +59 -59
- package/src/lib/get-product-price.ts +133 -133
- package/src/lib/media-image.tsx +39 -39
- package/src/lib/payment-constants.ts +53 -53
- package/src/lib/platform.ts +13 -13
- package/src/lib/price.tsx +39 -39
- package/src/lib/store-api-error.ts +36 -36
- package/src/lib/variant-caption.ts +32 -32
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
|
@@ -1,224 +1,224 @@
|
|
|
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 { applyGiftCard, removeGiftCard } from "../api/gift-cards"
|
|
8
|
-
import { Price } from "../lib/price"
|
|
9
|
-
import { cn } from "../lib/utils"
|
|
10
|
-
import { useCheckoutLabels } from "./context"
|
|
11
|
-
import { translateGiftCardError } from "./payment-error-copy"
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* GiftCardSection — collapsible gift-card input shown inside the order
|
|
15
|
-
* summary card, plus the applied-cards chip list (masked `last4`, per-card
|
|
16
|
-
* coverage, remove).
|
|
17
|
-
*
|
|
18
|
-
* NEW in the Cartbase port (no @1click equivalent — gift-card tender is a
|
|
19
|
-
* Cartbase feature, docs/storefront/gift-cards.md). Built in the
|
|
20
|
-
* DiscountSection idiom so the two summary widgets read as one family.
|
|
21
|
-
*
|
|
22
|
-
* Contract discipline baked in:
|
|
23
|
-
* - Redemption is TENDER, never a discount: totals/VAT don't move. This
|
|
24
|
-
* component renders `cart.gift_cards[]` / `gift_card_total` /
|
|
25
|
-
* `gift_card_remainder` straight off the cart decoration — SERVER
|
|
26
|
-
* truth, zero client math.
|
|
27
|
-
* - Apply/remove via `@cartbase/storefront/api/gift-cards`
|
|
28
|
-
* (POST/DELETE /api/store/carts/:id/gift-cards). Both return the
|
|
29
|
-
* decorated cart — handed to `onCartChange`.
|
|
30
|
-
* - The API deliberately answers unknown/disabled/expired/depleted codes
|
|
31
|
-
* with ONE generic `invalid_gift_card` (no code-existence oracle) and
|
|
32
|
-
* rate-limits attempts (`rate_limited`). The error copy mirrors that:
|
|
33
|
-
* one honest generic message per code — never branch on failure reasons
|
|
34
|
-
* the API hides (`translateGiftCardError`).
|
|
35
|
-
* - The confirmation is MASKED — only `last4` renders; the code is shown
|
|
36
|
-
* nowhere after submit.
|
|
37
|
-
*
|
|
38
|
-
* After apply/remove, the checkout must re-sync any pending provider
|
|
39
|
-
* session (`syncPaymentAmount`) — the orchestration hook exposes it; wire
|
|
40
|
-
* `onCartChange` to call it (see CheckoutClient).
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
type GiftCardSectionProps = {
|
|
44
|
-
/** The SDK transport — apply/remove go through it. */
|
|
45
|
-
client: StorefrontClient
|
|
46
|
-
cart: Cart
|
|
47
|
-
/** Receives the decorated cart returned by a successful apply/remove. */
|
|
48
|
-
onCartChange?: (cart: Cart) => void
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function GiftCardSection({
|
|
52
|
-
client,
|
|
53
|
-
cart,
|
|
54
|
-
onCartChange,
|
|
55
|
-
}: GiftCardSectionProps) {
|
|
56
|
-
const labels = useCheckoutLabels()
|
|
57
|
-
const [code, setCode] = useState("")
|
|
58
|
-
const [loading, setLoading] = useState(false)
|
|
59
|
-
const [removingId, setRemovingId] = useState<string | null>(null)
|
|
60
|
-
const [error, setError] = useState("")
|
|
61
|
-
const [open, setOpen] = useState(false)
|
|
62
|
-
|
|
63
|
-
const appliedCards = cart.gift_cards ?? []
|
|
64
|
-
|
|
65
|
-
const handleApply = async () => {
|
|
66
|
-
if (!code.trim()) return
|
|
67
|
-
setLoading(true)
|
|
68
|
-
setError("")
|
|
69
|
-
try {
|
|
70
|
-
const { cart: updated } = await applyGiftCard(client, cart.id, {
|
|
71
|
-
code: code.trim(),
|
|
72
|
-
})
|
|
73
|
-
onCartChange?.(updated)
|
|
74
|
-
setCode("")
|
|
75
|
-
} catch (e: unknown) {
|
|
76
|
-
// Generic-oracle discipline: one clean message per code family.
|
|
77
|
-
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
78
|
-
} finally {
|
|
79
|
-
setLoading(false)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const handleRemove = async (giftCardId: string) => {
|
|
84
|
-
setRemovingId(giftCardId)
|
|
85
|
-
setError("")
|
|
86
|
-
try {
|
|
87
|
-
const { cart: updated } = await removeGiftCard(client, cart.id, {
|
|
88
|
-
gift_card_id: giftCardId,
|
|
89
|
-
})
|
|
90
|
-
onCartChange?.(updated)
|
|
91
|
-
} catch (e: unknown) {
|
|
92
|
-
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
93
|
-
} finally {
|
|
94
|
-
setRemovingId(null)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<div>
|
|
100
|
-
<button
|
|
101
|
-
type="button"
|
|
102
|
-
onClick={() => setOpen(!open)}
|
|
103
|
-
className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
|
|
104
|
-
>
|
|
105
|
-
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center flex-shrink-0">
|
|
106
|
-
{/* Gift icon */}
|
|
107
|
-
<svg
|
|
108
|
-
className="w-5 h-5 text-card"
|
|
109
|
-
fill="none"
|
|
110
|
-
viewBox="0 0 24 24"
|
|
111
|
-
stroke="currentColor"
|
|
112
|
-
strokeWidth={2}
|
|
113
|
-
>
|
|
114
|
-
<path
|
|
115
|
-
strokeLinecap="round"
|
|
116
|
-
strokeLinejoin="round"
|
|
117
|
-
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H4.5a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
|
118
|
-
/>
|
|
119
|
-
</svg>
|
|
120
|
-
</div>
|
|
121
|
-
<div className="flex-1 text-left">
|
|
122
|
-
<p className="text-sm font-semibold text-foreground">
|
|
123
|
-
{labels.giftCard}
|
|
124
|
-
</p>
|
|
125
|
-
<p className="text-xs text-muted-foreground">{labels.giftCardSub}</p>
|
|
126
|
-
</div>
|
|
127
|
-
<span className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
|
128
|
-
{labels.addCode}
|
|
129
|
-
<svg
|
|
130
|
-
className={cn(
|
|
131
|
-
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
132
|
-
open && "rotate-180"
|
|
133
|
-
)}
|
|
134
|
-
fill="none"
|
|
135
|
-
viewBox="0 0 24 24"
|
|
136
|
-
stroke="currentColor"
|
|
137
|
-
strokeWidth={2}
|
|
138
|
-
>
|
|
139
|
-
<path
|
|
140
|
-
strokeLinecap="round"
|
|
141
|
-
strokeLinejoin="round"
|
|
142
|
-
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
143
|
-
/>
|
|
144
|
-
</svg>
|
|
145
|
-
</span>
|
|
146
|
-
</button>
|
|
147
|
-
|
|
148
|
-
{open && (
|
|
149
|
-
<div className="mt-3 flex gap-2">
|
|
150
|
-
<input
|
|
151
|
-
type="text"
|
|
152
|
-
value={code}
|
|
153
|
-
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
154
|
-
placeholder={labels.giftCardPlaceholder}
|
|
155
|
-
className="flex-1 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
|
|
156
|
-
/>
|
|
157
|
-
<button
|
|
158
|
-
type="button"
|
|
159
|
-
onClick={handleApply}
|
|
160
|
-
disabled={loading || !code.trim()}
|
|
161
|
-
className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
|
|
162
|
-
>
|
|
163
|
-
{labels.addCode}
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
169
|
-
|
|
170
|
-
{appliedCards.length > 0 && (
|
|
171
|
-
<div className="mt-3 space-y-1.5">
|
|
172
|
-
{appliedCards.map((gc) => (
|
|
173
|
-
<div
|
|
174
|
-
key={gc.id}
|
|
175
|
-
className={cn(
|
|
176
|
-
"flex items-center justify-between px-4 py-2.5 bg-success/10 border border-success/20 rounded-lg transition-opacity",
|
|
177
|
-
removingId === gc.id && "opacity-40 pointer-events-none"
|
|
178
|
-
)}
|
|
179
|
-
>
|
|
180
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
181
|
-
<svg
|
|
182
|
-
className="w-4 h-4 text-success flex-shrink-0"
|
|
183
|
-
fill="none"
|
|
184
|
-
viewBox="0 0 24 24"
|
|
185
|
-
stroke="currentColor"
|
|
186
|
-
strokeWidth={2}
|
|
187
|
-
>
|
|
188
|
-
<path
|
|
189
|
-
strokeLinecap="round"
|
|
190
|
-
strokeLinejoin="round"
|
|
191
|
-
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
192
|
-
/>
|
|
193
|
-
</svg>
|
|
194
|
-
<span className="text-sm font-semibold text-success truncate">
|
|
195
|
-
{labels.giftCardEnding} ••••{gc.last4}
|
|
196
|
-
</span>
|
|
197
|
-
</div>
|
|
198
|
-
<div className="flex items-center gap-3 flex-shrink-0">
|
|
199
|
-
{/* Per-card coverage — server-derived; a depleted/disabled
|
|
200
|
-
card stays listed at 0 so the shopper sees WHY the
|
|
201
|
-
remainder didn't shrink. */}
|
|
202
|
-
<span className="text-sm font-medium text-success">
|
|
203
|
-
-
|
|
204
|
-
<Price
|
|
205
|
-
amount={gc.amount}
|
|
206
|
-
currencyCode={cart.currency_code}
|
|
207
|
-
/>
|
|
208
|
-
</span>
|
|
209
|
-
<button
|
|
210
|
-
type="button"
|
|
211
|
-
onClick={() => handleRemove(gc.id)}
|
|
212
|
-
disabled={removingId !== null}
|
|
213
|
-
className="text-xs text-muted-foreground hover:text-destructive hover:underline transition-colors"
|
|
214
|
-
>
|
|
215
|
-
{labels.remove}
|
|
216
|
-
</button>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
))}
|
|
220
|
-
</div>
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
)
|
|
224
|
-
}
|
|
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 { applyGiftCard, removeGiftCard } from "../api/gift-cards"
|
|
8
|
+
import { Price } from "../lib/price"
|
|
9
|
+
import { cn } from "../lib/utils"
|
|
10
|
+
import { useCheckoutLabels } from "./context"
|
|
11
|
+
import { translateGiftCardError } from "./payment-error-copy"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GiftCardSection — collapsible gift-card input shown inside the order
|
|
15
|
+
* summary card, plus the applied-cards chip list (masked `last4`, per-card
|
|
16
|
+
* coverage, remove).
|
|
17
|
+
*
|
|
18
|
+
* NEW in the Cartbase port (no @1click equivalent — gift-card tender is a
|
|
19
|
+
* Cartbase feature, docs/storefront/gift-cards.md). Built in the
|
|
20
|
+
* DiscountSection idiom so the two summary widgets read as one family.
|
|
21
|
+
*
|
|
22
|
+
* Contract discipline baked in:
|
|
23
|
+
* - Redemption is TENDER, never a discount: totals/VAT don't move. This
|
|
24
|
+
* component renders `cart.gift_cards[]` / `gift_card_total` /
|
|
25
|
+
* `gift_card_remainder` straight off the cart decoration — SERVER
|
|
26
|
+
* truth, zero client math.
|
|
27
|
+
* - Apply/remove via `@cartbase/storefront/api/gift-cards`
|
|
28
|
+
* (POST/DELETE /api/store/carts/:id/gift-cards). Both return the
|
|
29
|
+
* decorated cart — handed to `onCartChange`.
|
|
30
|
+
* - The API deliberately answers unknown/disabled/expired/depleted codes
|
|
31
|
+
* with ONE generic `invalid_gift_card` (no code-existence oracle) and
|
|
32
|
+
* rate-limits attempts (`rate_limited`). The error copy mirrors that:
|
|
33
|
+
* one honest generic message per code — never branch on failure reasons
|
|
34
|
+
* the API hides (`translateGiftCardError`).
|
|
35
|
+
* - The confirmation is MASKED — only `last4` renders; the code is shown
|
|
36
|
+
* nowhere after submit.
|
|
37
|
+
*
|
|
38
|
+
* After apply/remove, the checkout must re-sync any pending provider
|
|
39
|
+
* session (`syncPaymentAmount`) — the orchestration hook exposes it; wire
|
|
40
|
+
* `onCartChange` to call it (see CheckoutClient).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
type GiftCardSectionProps = {
|
|
44
|
+
/** The SDK transport — apply/remove go through it. */
|
|
45
|
+
client: StorefrontClient
|
|
46
|
+
cart: Cart
|
|
47
|
+
/** Receives the decorated cart returned by a successful apply/remove. */
|
|
48
|
+
onCartChange?: (cart: Cart) => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function GiftCardSection({
|
|
52
|
+
client,
|
|
53
|
+
cart,
|
|
54
|
+
onCartChange,
|
|
55
|
+
}: GiftCardSectionProps) {
|
|
56
|
+
const labels = useCheckoutLabels()
|
|
57
|
+
const [code, setCode] = useState("")
|
|
58
|
+
const [loading, setLoading] = useState(false)
|
|
59
|
+
const [removingId, setRemovingId] = useState<string | null>(null)
|
|
60
|
+
const [error, setError] = useState("")
|
|
61
|
+
const [open, setOpen] = useState(false)
|
|
62
|
+
|
|
63
|
+
const appliedCards = cart.gift_cards ?? []
|
|
64
|
+
|
|
65
|
+
const handleApply = async () => {
|
|
66
|
+
if (!code.trim()) return
|
|
67
|
+
setLoading(true)
|
|
68
|
+
setError("")
|
|
69
|
+
try {
|
|
70
|
+
const { cart: updated } = await applyGiftCard(client, cart.id, {
|
|
71
|
+
code: code.trim(),
|
|
72
|
+
})
|
|
73
|
+
onCartChange?.(updated)
|
|
74
|
+
setCode("")
|
|
75
|
+
} catch (e: unknown) {
|
|
76
|
+
// Generic-oracle discipline: one clean message per code family.
|
|
77
|
+
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleRemove = async (giftCardId: string) => {
|
|
84
|
+
setRemovingId(giftCardId)
|
|
85
|
+
setError("")
|
|
86
|
+
try {
|
|
87
|
+
const { cart: updated } = await removeGiftCard(client, cart.id, {
|
|
88
|
+
gift_card_id: giftCardId,
|
|
89
|
+
})
|
|
90
|
+
onCartChange?.(updated)
|
|
91
|
+
} catch (e: unknown) {
|
|
92
|
+
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
93
|
+
} finally {
|
|
94
|
+
setRemovingId(null)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => setOpen(!open)}
|
|
103
|
+
className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
|
|
104
|
+
>
|
|
105
|
+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-primary/70 flex items-center justify-center flex-shrink-0">
|
|
106
|
+
{/* Gift icon */}
|
|
107
|
+
<svg
|
|
108
|
+
className="w-5 h-5 text-card"
|
|
109
|
+
fill="none"
|
|
110
|
+
viewBox="0 0 24 24"
|
|
111
|
+
stroke="currentColor"
|
|
112
|
+
strokeWidth={2}
|
|
113
|
+
>
|
|
114
|
+
<path
|
|
115
|
+
strokeLinecap="round"
|
|
116
|
+
strokeLinejoin="round"
|
|
117
|
+
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H4.5a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
|
118
|
+
/>
|
|
119
|
+
</svg>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="flex-1 text-left">
|
|
122
|
+
<p className="text-sm font-semibold text-foreground">
|
|
123
|
+
{labels.giftCard}
|
|
124
|
+
</p>
|
|
125
|
+
<p className="text-xs text-muted-foreground">{labels.giftCardSub}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<span className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
|
128
|
+
{labels.addCode}
|
|
129
|
+
<svg
|
|
130
|
+
className={cn(
|
|
131
|
+
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
132
|
+
open && "rotate-180"
|
|
133
|
+
)}
|
|
134
|
+
fill="none"
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
stroke="currentColor"
|
|
137
|
+
strokeWidth={2}
|
|
138
|
+
>
|
|
139
|
+
<path
|
|
140
|
+
strokeLinecap="round"
|
|
141
|
+
strokeLinejoin="round"
|
|
142
|
+
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
143
|
+
/>
|
|
144
|
+
</svg>
|
|
145
|
+
</span>
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
{open && (
|
|
149
|
+
<div className="mt-3 flex gap-2">
|
|
150
|
+
<input
|
|
151
|
+
type="text"
|
|
152
|
+
value={code}
|
|
153
|
+
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
154
|
+
placeholder={labels.giftCardPlaceholder}
|
|
155
|
+
className="flex-1 h-11 px-4 text-sm bg-card border border-border rounded-xl focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all"
|
|
156
|
+
/>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
onClick={handleApply}
|
|
160
|
+
disabled={loading || !code.trim()}
|
|
161
|
+
className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
|
|
162
|
+
>
|
|
163
|
+
{labels.addCode}
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
169
|
+
|
|
170
|
+
{appliedCards.length > 0 && (
|
|
171
|
+
<div className="mt-3 space-y-1.5">
|
|
172
|
+
{appliedCards.map((gc) => (
|
|
173
|
+
<div
|
|
174
|
+
key={gc.id}
|
|
175
|
+
className={cn(
|
|
176
|
+
"flex items-center justify-between px-4 py-2.5 bg-success/10 border border-success/20 rounded-lg transition-opacity",
|
|
177
|
+
removingId === gc.id && "opacity-40 pointer-events-none"
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
181
|
+
<svg
|
|
182
|
+
className="w-4 h-4 text-success flex-shrink-0"
|
|
183
|
+
fill="none"
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
stroke="currentColor"
|
|
186
|
+
strokeWidth={2}
|
|
187
|
+
>
|
|
188
|
+
<path
|
|
189
|
+
strokeLinecap="round"
|
|
190
|
+
strokeLinejoin="round"
|
|
191
|
+
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
192
|
+
/>
|
|
193
|
+
</svg>
|
|
194
|
+
<span className="text-sm font-semibold text-success truncate">
|
|
195
|
+
{labels.giftCardEnding} ••••{gc.last4}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex items-center gap-3 flex-shrink-0">
|
|
199
|
+
{/* Per-card coverage — server-derived; a depleted/disabled
|
|
200
|
+
card stays listed at 0 so the shopper sees WHY the
|
|
201
|
+
remainder didn't shrink. */}
|
|
202
|
+
<span className="text-sm font-medium text-success">
|
|
203
|
+
-
|
|
204
|
+
<Price
|
|
205
|
+
amount={gc.amount}
|
|
206
|
+
currencyCode={cart.currency_code}
|
|
207
|
+
/>
|
|
208
|
+
</span>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={() => handleRemove(gc.id)}
|
|
212
|
+
disabled={removingId !== null}
|
|
213
|
+
className="text-xs text-muted-foreground hover:text-destructive hover:underline transition-colors"
|
|
214
|
+
>
|
|
215
|
+
{labels.remove}
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
package/src/checkout/index.ts
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
// @cartbase/storefront/checkout — barrel export.
|
|
2
|
-
//
|
|
3
|
-
// Ported from `@1click/ui/src/checkout/index.ts` (v2.3.1). Not ported in
|
|
4
|
-
// this batch: CheckoutLineItem's standalone file (its flat row lives inside
|
|
5
|
-
// order-summary.tsx and is re-exported below) and the Mobile* bars (out of
|
|
6
|
-
// the pre-declared file list; stores compose their own from the hook).
|
|
7
|
-
// Extra modules beyond the pre-declared export list (geocode,
|
|
8
|
-
// compare-addresses) are exported from here only.
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
CheckoutProvider,
|
|
12
|
-
useCheckoutLabels,
|
|
13
|
-
useOrderConfirmedPath,
|
|
14
|
-
} from "./context"
|
|
15
|
-
export { defaultCheckoutLabels, type CheckoutLabels } from "./labels"
|
|
16
|
-
export { ErrorMessage } from "./error-message"
|
|
17
|
-
export {
|
|
18
|
-
StripeContext,
|
|
19
|
-
StripeScopeProvider,
|
|
20
|
-
StripeElementsScope,
|
|
21
|
-
useStripeScope,
|
|
22
|
-
} from "./stripe-wrapper"
|
|
23
|
-
export { PaymentWrapper } from "./payment-wrapper"
|
|
24
|
-
export { PaymentButton } from "./payment-button"
|
|
25
|
-
export {
|
|
26
|
-
EcontOfficeSelector,
|
|
27
|
-
type EcontOffice,
|
|
28
|
-
} from "./econt-office-selector"
|
|
29
|
-
export {
|
|
30
|
-
BoxNowLockerSelector,
|
|
31
|
-
type BoxNowLocker,
|
|
32
|
-
} from "./boxnow-locker-selector"
|
|
33
|
-
export { AddressSelect } from "./address-select"
|
|
34
|
-
export { CompanyDetails } from "./company-details"
|
|
35
|
-
export { DiscountSection } from "./discount-section"
|
|
36
|
-
export { GiftCardSection } from "./gift-card-section"
|
|
37
|
-
export { LineItemCard } from "./line-item-card"
|
|
38
|
-
export { OrderSummary, CheckoutLineItem } from "./order-summary"
|
|
39
|
-
export { CheckoutAddressForm } from "./address-form"
|
|
40
|
-
export { CheckoutShippingMethodList } from "./shipping-method-list"
|
|
41
|
-
export { CheckoutPaymentMethodList } from "./payment-method-list"
|
|
42
|
-
export { CheckoutClient } from "./checkout-client"
|
|
43
|
-
export {
|
|
44
|
-
useCheckoutOrchestration,
|
|
45
|
-
resolveOrderConfirmedPath,
|
|
46
|
-
type UseCheckoutOrchestrationOptions,
|
|
47
|
-
type CheckoutLogError,
|
|
48
|
-
type PaymentProviderLike,
|
|
49
|
-
} from "./use-checkout-orchestration"
|
|
50
|
-
export {
|
|
51
|
-
translatePaymentError,
|
|
52
|
-
translateGiftCardError,
|
|
53
|
-
paymentErrorKey,
|
|
54
|
-
giftCardErrorKey,
|
|
55
|
-
} from "./payment-error-copy"
|
|
56
|
-
export {
|
|
57
|
-
translateAddressError,
|
|
58
|
-
addressErrorKey,
|
|
59
|
-
} from "./address-error-copy"
|
|
60
|
-
export {
|
|
61
|
-
translatePromotionError,
|
|
62
|
-
promotionErrorKey,
|
|
63
|
-
} from "./promotion-error-copy"
|
|
64
|
-
export { default as compareAddresses } from "./compare-addresses"
|
|
65
|
-
export {
|
|
66
|
-
transliterateBgToLatin,
|
|
67
|
-
normalizeForMatch,
|
|
68
|
-
distanceMeters,
|
|
69
|
-
formatDistance,
|
|
70
|
-
cleanAddress,
|
|
71
|
-
geocodeAddress,
|
|
72
|
-
} from "./geocode"
|
|
1
|
+
// @cartbase/storefront/checkout — barrel export.
|
|
2
|
+
//
|
|
3
|
+
// Ported from `@1click/ui/src/checkout/index.ts` (v2.3.1). Not ported in
|
|
4
|
+
// this batch: CheckoutLineItem's standalone file (its flat row lives inside
|
|
5
|
+
// order-summary.tsx and is re-exported below) and the Mobile* bars (out of
|
|
6
|
+
// the pre-declared file list; stores compose their own from the hook).
|
|
7
|
+
// Extra modules beyond the pre-declared export list (geocode,
|
|
8
|
+
// compare-addresses) are exported from here only.
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
CheckoutProvider,
|
|
12
|
+
useCheckoutLabels,
|
|
13
|
+
useOrderConfirmedPath,
|
|
14
|
+
} from "./context"
|
|
15
|
+
export { defaultCheckoutLabels, type CheckoutLabels } from "./labels"
|
|
16
|
+
export { ErrorMessage } from "./error-message"
|
|
17
|
+
export {
|
|
18
|
+
StripeContext,
|
|
19
|
+
StripeScopeProvider,
|
|
20
|
+
StripeElementsScope,
|
|
21
|
+
useStripeScope,
|
|
22
|
+
} from "./stripe-wrapper"
|
|
23
|
+
export { PaymentWrapper } from "./payment-wrapper"
|
|
24
|
+
export { PaymentButton } from "./payment-button"
|
|
25
|
+
export {
|
|
26
|
+
EcontOfficeSelector,
|
|
27
|
+
type EcontOffice,
|
|
28
|
+
} from "./econt-office-selector"
|
|
29
|
+
export {
|
|
30
|
+
BoxNowLockerSelector,
|
|
31
|
+
type BoxNowLocker,
|
|
32
|
+
} from "./boxnow-locker-selector"
|
|
33
|
+
export { AddressSelect } from "./address-select"
|
|
34
|
+
export { CompanyDetails } from "./company-details"
|
|
35
|
+
export { DiscountSection } from "./discount-section"
|
|
36
|
+
export { GiftCardSection } from "./gift-card-section"
|
|
37
|
+
export { LineItemCard } from "./line-item-card"
|
|
38
|
+
export { OrderSummary, CheckoutLineItem } from "./order-summary"
|
|
39
|
+
export { CheckoutAddressForm } from "./address-form"
|
|
40
|
+
export { CheckoutShippingMethodList } from "./shipping-method-list"
|
|
41
|
+
export { CheckoutPaymentMethodList } from "./payment-method-list"
|
|
42
|
+
export { CheckoutClient } from "./checkout-client"
|
|
43
|
+
export {
|
|
44
|
+
useCheckoutOrchestration,
|
|
45
|
+
resolveOrderConfirmedPath,
|
|
46
|
+
type UseCheckoutOrchestrationOptions,
|
|
47
|
+
type CheckoutLogError,
|
|
48
|
+
type PaymentProviderLike,
|
|
49
|
+
} from "./use-checkout-orchestration"
|
|
50
|
+
export {
|
|
51
|
+
translatePaymentError,
|
|
52
|
+
translateGiftCardError,
|
|
53
|
+
paymentErrorKey,
|
|
54
|
+
giftCardErrorKey,
|
|
55
|
+
} from "./payment-error-copy"
|
|
56
|
+
export {
|
|
57
|
+
translateAddressError,
|
|
58
|
+
addressErrorKey,
|
|
59
|
+
} from "./address-error-copy"
|
|
60
|
+
export {
|
|
61
|
+
translatePromotionError,
|
|
62
|
+
promotionErrorKey,
|
|
63
|
+
} from "./promotion-error-copy"
|
|
64
|
+
export { default as compareAddresses } from "./compare-addresses"
|
|
65
|
+
export {
|
|
66
|
+
transliterateBgToLatin,
|
|
67
|
+
normalizeForMatch,
|
|
68
|
+
distanceMeters,
|
|
69
|
+
formatDistance,
|
|
70
|
+
cleanAddress,
|
|
71
|
+
geocodeAddress,
|
|
72
|
+
} from "./geocode"
|