@cartbase/storefront 0.8.0 → 0.10.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 +7 -8
- package/src/cart-drawer/cross-sell-carousel.tsx +2 -3
- package/src/cart-drawer/gift-wrap.tsx +82 -83
- package/src/cart-drawer/index.ts +0 -1
- package/src/cart-drawer/item/index.tsx +4 -6
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/checkout/address-error-copy.ts +117 -119
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -0
- package/src/checkout/gift-card-section.tsx +4 -4
- package/src/checkout/index.ts +4 -5
- package/src/checkout/labels.ts +218 -0
- package/src/checkout/line-item-card.tsx +3 -3
- package/src/checkout/order-summary.tsx +11 -12
- package/src/checkout/payment-button.tsx +372 -373
- package/src/checkout/payment-error-copy.ts +154 -269
- package/src/checkout/promotion-error-copy.ts +91 -124
- package/src/checkout/shipping-method-list.tsx +2 -2
- package/src/checkout/use-checkout-orchestration.ts +10 -8
- package/src/lib/money.ts +1 -1
- package/src/lib/price.tsx +39 -0
- package/src/locales/bg.ts +468 -0
- package/src/locales/en.ts +26 -0
- package/src/locales/es.ts +467 -0
- package/src/locales/index.ts +19 -0
- package/src/locales/provider.tsx +63 -0
- package/src/locales/types.ts +49 -0
- package/src/order/index.ts +0 -1
- package/src/order/order-delivery-card.tsx +2 -2
- package/src/order/order-item.tsx +3 -3
- package/src/order/order-totals.tsx +4 -4
- package/src/products/index.ts +0 -1
- package/src/reviews-ui/index.ts +0 -1
- package/src/store/index.ts +0 -1
- package/src/tracking/attribution.ts +13 -0
- package/src/tracking/chatgpt-pixel.tsx +92 -0
- package/src/tracking/consent-banner.tsx +5 -1
- package/src/tracking/consent.ts +12 -0
- package/src/tracking/events.ts +56 -2
- package/src/tracking/index.ts +12 -0
- package/src/tracking/oaiq.ts +206 -0
- package/src/tracking/storefront-tags.tsx +2 -0
- package/src/tracking/types.ts +4 -0
- package/src/cart-drawer/labels-bg.ts +0 -72
- package/src/checkout/labels-bg.ts +0 -128
- package/src/lib/dual-price.tsx +0 -73
- package/src/order/labels-bg.ts +0 -39
- package/src/products/labels-bg.ts +0 -35
- package/src/reviews-ui/labels-bg.ts +0 -91
- package/src/store/labels-bg.ts +0 -22
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
|
|
5
|
-
import type { StorefrontClient } from "../api/http"
|
|
6
|
-
import { applyPromotions, type Cart } from "../api/carts"
|
|
7
|
-
import { convertToLocale } from "../lib/money"
|
|
8
|
-
import { cn } from "../lib/utils"
|
|
9
|
-
import { useCheckoutLabels } from "./context"
|
|
10
|
-
import { translatePromotionError } from "./promotion-error-copy"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* DiscountSection — collapsible promo-code input shown inside the order
|
|
14
|
-
* summary card. Lists currently applied promotions with their discount
|
|
15
|
-
* amount.
|
|
16
|
-
*
|
|
17
|
-
* Ported from `@1click/ui/src/checkout/discount-section.tsx` (v2.3.1) with
|
|
18
|
-
* the Cartbase data seam:
|
|
19
|
-
*
|
|
20
|
-
* - `applyPromotions(codes)` (Medusa js-sdk server action, full-list
|
|
21
|
-
* replace) → `POST /api/store/carts/:id/promotions {promo_codes}` — the
|
|
22
|
-
* Cartbase route is ADDITIVE (upserts each code's link, then re-applies),
|
|
23
|
-
* so only the NEW code is sent; already-applied codes stay put. The SDK
|
|
24
|
-
* carts module ships no wrapper for this route yet, so the component
|
|
25
|
-
* calls it through the client transport with the documented DTO
|
|
26
|
-
* (verified against src/app/api/store/carts/[id]/promotions/route.ts).
|
|
27
|
-
* - `cart.promotions` on the Cartbase wire is the raw pivot embed
|
|
28
|
-
* (`[{promotion: {..., application_method}}]`) — unwrapped here, with a
|
|
29
|
-
* flat-row fallback should the decoration ever flatten it.
|
|
30
|
-
* - Errors are code-first (`promotion_not_found` / `promotion_inactive` /
|
|
31
|
-
* `promotion_misconfigured` / `promotion_unsupported_type` + cart-level
|
|
32
|
-
* codes) via `translatePromotionError`.
|
|
33
|
-
*
|
|
34
|
-
* The route returns the freshly decorated cart; `onCartChange` hands it to
|
|
35
|
-
* the host (or the host calls `router.refresh()` in an RSC app).
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/** Structural shape of one applied promotion (promotions row). */
|
|
39
|
-
type AppliedPromotion = {
|
|
40
|
-
id: string
|
|
41
|
-
code: string | null
|
|
42
|
-
application_method?: {
|
|
43
|
-
type?: string | null
|
|
44
|
-
value?: number | string | null
|
|
45
|
-
currency_code?: string | null
|
|
46
|
-
} | null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Unwrap the Cartbase pivot embed; tolerate an already-flat row. */
|
|
50
|
-
function extractPromotions(cart: Cart): AppliedPromotion[] {
|
|
51
|
-
const raw = (cart.promotions ?? []) as Array<Record<string, unknown>>
|
|
52
|
-
return raw
|
|
53
|
-
.map((row) =>
|
|
54
|
-
row && typeof row === "object" && "promotion" in row
|
|
55
|
-
? (row.promotion as AppliedPromotion | null)
|
|
56
|
-
: (row as unknown as AppliedPromotion)
|
|
57
|
-
)
|
|
58
|
-
.filter((p): p is AppliedPromotion => !!p && typeof p.id === "string")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type DiscountSectionProps = {
|
|
62
|
-
/** The SDK transport — the promotions route is called through it. */
|
|
63
|
-
client: StorefrontClient
|
|
64
|
-
cart: Cart
|
|
65
|
-
/** Receives the decorated cart returned by a successful apply. */
|
|
66
|
-
onCartChange?: (cart: Cart) => void
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function DiscountSection({
|
|
70
|
-
client,
|
|
71
|
-
cart,
|
|
72
|
-
onCartChange,
|
|
73
|
-
}: DiscountSectionProps) {
|
|
74
|
-
const labels = useCheckoutLabels()
|
|
75
|
-
const [code, setCode] = useState("")
|
|
76
|
-
const [loading, setLoading] = useState(false)
|
|
77
|
-
const [error, setError] = useState("")
|
|
78
|
-
const [open, setOpen] = useState(false)
|
|
79
|
-
|
|
80
|
-
const promotions = extractPromotions(cart)
|
|
81
|
-
|
|
82
|
-
const handleApply = async () => {
|
|
83
|
-
if (!code.trim()) return
|
|
84
|
-
setLoading(true)
|
|
85
|
-
setError("")
|
|
86
|
-
try {
|
|
87
|
-
// Additive apply — the Cartbase route upserts the new code's link and
|
|
88
|
-
// re-applies every cart promotion; already-applied codes stay.
|
|
89
|
-
const { cart: updated } = await applyPromotions(client, cart.id, [code.trim()])
|
|
90
|
-
onCartChange?.(updated)
|
|
91
|
-
setCode("")
|
|
92
|
-
} catch (e: unknown) {
|
|
93
|
-
// Never surface the raw API error string to the shopper — map to a
|
|
94
|
-
// proper localized message (code-first, pattern fallback).
|
|
95
|
-
setError(translatePromotionError(e, { hasEmail: !!cart.email }))
|
|
96
|
-
} finally {
|
|
97
|
-
setLoading(false)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div>
|
|
103
|
-
<button
|
|
104
|
-
type="button"
|
|
105
|
-
onClick={() => setOpen(!open)}
|
|
106
|
-
className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
|
|
107
|
-
>
|
|
108
|
-
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-warning to-warning/70 flex items-center justify-center flex-shrink-0">
|
|
109
|
-
<svg
|
|
110
|
-
className="w-5 h-5 text-card"
|
|
111
|
-
fill="none"
|
|
112
|
-
viewBox="0 0 24 24"
|
|
113
|
-
stroke="currentColor"
|
|
114
|
-
strokeWidth={2}
|
|
115
|
-
>
|
|
116
|
-
<path
|
|
117
|
-
strokeLinecap="round"
|
|
118
|
-
strokeLinejoin="round"
|
|
119
|
-
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
|
120
|
-
/>
|
|
121
|
-
<path
|
|
122
|
-
strokeLinecap="round"
|
|
123
|
-
strokeLinejoin="round"
|
|
124
|
-
d="M6 6h.008v.008H6V6z"
|
|
125
|
-
/>
|
|
126
|
-
</svg>
|
|
127
|
-
</div>
|
|
128
|
-
<div className="flex-1 text-left">
|
|
129
|
-
<p className="text-sm font-semibold text-foreground">
|
|
130
|
-
{labels.discountCode}
|
|
131
|
-
</p>
|
|
132
|
-
<p className="text-xs text-muted-foreground">{labels.discountSub}</p>
|
|
133
|
-
</div>
|
|
134
|
-
<span className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
|
135
|
-
{labels.addCode}
|
|
136
|
-
<svg
|
|
137
|
-
className={cn(
|
|
138
|
-
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
139
|
-
open && "rotate-180"
|
|
140
|
-
)}
|
|
141
|
-
fill="none"
|
|
142
|
-
viewBox="0 0 24 24"
|
|
143
|
-
stroke="currentColor"
|
|
144
|
-
strokeWidth={2}
|
|
145
|
-
>
|
|
146
|
-
<path
|
|
147
|
-
strokeLinecap="round"
|
|
148
|
-
strokeLinejoin="round"
|
|
149
|
-
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
150
|
-
/>
|
|
151
|
-
</svg>
|
|
152
|
-
</span>
|
|
153
|
-
</button>
|
|
154
|
-
|
|
155
|
-
{open && (
|
|
156
|
-
<div className="mt-3 flex gap-2">
|
|
157
|
-
<input
|
|
158
|
-
type="text"
|
|
159
|
-
value={code}
|
|
160
|
-
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
161
|
-
placeholder="PROMO2024"
|
|
162
|
-
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"
|
|
163
|
-
/>
|
|
164
|
-
<button
|
|
165
|
-
type="button"
|
|
166
|
-
onClick={handleApply}
|
|
167
|
-
disabled={loading || !code.trim()}
|
|
168
|
-
className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
|
|
169
|
-
>
|
|
170
|
-
{labels.addCode}
|
|
171
|
-
</button>
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
|
|
175
|
-
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
176
|
-
|
|
177
|
-
{promotions.length > 0 && (
|
|
178
|
-
<div className="mt-3 space-y-1.5">
|
|
179
|
-
{promotions.map((p) => (
|
|
180
|
-
<div
|
|
181
|
-
key={p.id}
|
|
182
|
-
className="flex items-center justify-between px-4 py-2.5 bg-success/10 border border-success/20 rounded-lg"
|
|
183
|
-
>
|
|
184
|
-
<div className="flex items-center gap-2">
|
|
185
|
-
<svg
|
|
186
|
-
className="w-4 h-4 text-success"
|
|
187
|
-
fill="none"
|
|
188
|
-
viewBox="0 0 24 24"
|
|
189
|
-
stroke="currentColor"
|
|
190
|
-
strokeWidth={2}
|
|
191
|
-
>
|
|
192
|
-
<path
|
|
193
|
-
strokeLinecap="round"
|
|
194
|
-
strokeLinejoin="round"
|
|
195
|
-
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
196
|
-
/>
|
|
197
|
-
</svg>
|
|
198
|
-
<span className="text-sm font-semibold text-success">
|
|
199
|
-
{p.code}
|
|
200
|
-
</span>
|
|
201
|
-
</div>
|
|
202
|
-
<span className="text-sm font-medium text-success">
|
|
203
|
-
{p.application_method?.type === "percentage"
|
|
204
|
-
? `-${p.application_method.value}%`
|
|
205
|
-
: p.application_method?.value && p.application_method?.currency_code
|
|
206
|
-
? `-${convertToLocale({
|
|
207
|
-
amount: +p.application_method.value,
|
|
208
|
-
currency_code: p.application_method.currency_code,
|
|
209
|
-
})}`
|
|
210
|
-
: ""}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
213
|
-
))}
|
|
214
|
-
</div>
|
|
215
|
-
)}
|
|
216
|
-
</div>
|
|
217
|
-
)
|
|
218
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import type { StorefrontClient } from "../api/http"
|
|
6
|
+
import { applyPromotions, type Cart } from "../api/carts"
|
|
7
|
+
import { convertToLocale } from "../lib/money"
|
|
8
|
+
import { cn } from "../lib/utils"
|
|
9
|
+
import { useCheckoutLabels } from "./context"
|
|
10
|
+
import { translatePromotionError } from "./promotion-error-copy"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* DiscountSection — collapsible promo-code input shown inside the order
|
|
14
|
+
* summary card. Lists currently applied promotions with their discount
|
|
15
|
+
* amount.
|
|
16
|
+
*
|
|
17
|
+
* Ported from `@1click/ui/src/checkout/discount-section.tsx` (v2.3.1) with
|
|
18
|
+
* the Cartbase data seam:
|
|
19
|
+
*
|
|
20
|
+
* - `applyPromotions(codes)` (Medusa js-sdk server action, full-list
|
|
21
|
+
* replace) → `POST /api/store/carts/:id/promotions {promo_codes}` — the
|
|
22
|
+
* Cartbase route is ADDITIVE (upserts each code's link, then re-applies),
|
|
23
|
+
* so only the NEW code is sent; already-applied codes stay put. The SDK
|
|
24
|
+
* carts module ships no wrapper for this route yet, so the component
|
|
25
|
+
* calls it through the client transport with the documented DTO
|
|
26
|
+
* (verified against src/app/api/store/carts/[id]/promotions/route.ts).
|
|
27
|
+
* - `cart.promotions` on the Cartbase wire is the raw pivot embed
|
|
28
|
+
* (`[{promotion: {..., application_method}}]`) — unwrapped here, with a
|
|
29
|
+
* flat-row fallback should the decoration ever flatten it.
|
|
30
|
+
* - Errors are code-first (`promotion_not_found` / `promotion_inactive` /
|
|
31
|
+
* `promotion_misconfigured` / `promotion_unsupported_type` + cart-level
|
|
32
|
+
* codes) via `translatePromotionError`.
|
|
33
|
+
*
|
|
34
|
+
* The route returns the freshly decorated cart; `onCartChange` hands it to
|
|
35
|
+
* the host (or the host calls `router.refresh()` in an RSC app).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** Structural shape of one applied promotion (promotions row). */
|
|
39
|
+
type AppliedPromotion = {
|
|
40
|
+
id: string
|
|
41
|
+
code: string | null
|
|
42
|
+
application_method?: {
|
|
43
|
+
type?: string | null
|
|
44
|
+
value?: number | string | null
|
|
45
|
+
currency_code?: string | null
|
|
46
|
+
} | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Unwrap the Cartbase pivot embed; tolerate an already-flat row. */
|
|
50
|
+
function extractPromotions(cart: Cart): AppliedPromotion[] {
|
|
51
|
+
const raw = (cart.promotions ?? []) as Array<Record<string, unknown>>
|
|
52
|
+
return raw
|
|
53
|
+
.map((row) =>
|
|
54
|
+
row && typeof row === "object" && "promotion" in row
|
|
55
|
+
? (row.promotion as AppliedPromotion | null)
|
|
56
|
+
: (row as unknown as AppliedPromotion)
|
|
57
|
+
)
|
|
58
|
+
.filter((p): p is AppliedPromotion => !!p && typeof p.id === "string")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type DiscountSectionProps = {
|
|
62
|
+
/** The SDK transport — the promotions route is called through it. */
|
|
63
|
+
client: StorefrontClient
|
|
64
|
+
cart: Cart
|
|
65
|
+
/** Receives the decorated cart returned by a successful apply. */
|
|
66
|
+
onCartChange?: (cart: Cart) => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function DiscountSection({
|
|
70
|
+
client,
|
|
71
|
+
cart,
|
|
72
|
+
onCartChange,
|
|
73
|
+
}: DiscountSectionProps) {
|
|
74
|
+
const labels = useCheckoutLabels()
|
|
75
|
+
const [code, setCode] = useState("")
|
|
76
|
+
const [loading, setLoading] = useState(false)
|
|
77
|
+
const [error, setError] = useState("")
|
|
78
|
+
const [open, setOpen] = useState(false)
|
|
79
|
+
|
|
80
|
+
const promotions = extractPromotions(cart)
|
|
81
|
+
|
|
82
|
+
const handleApply = async () => {
|
|
83
|
+
if (!code.trim()) return
|
|
84
|
+
setLoading(true)
|
|
85
|
+
setError("")
|
|
86
|
+
try {
|
|
87
|
+
// Additive apply — the Cartbase route upserts the new code's link and
|
|
88
|
+
// re-applies every cart promotion; already-applied codes stay.
|
|
89
|
+
const { cart: updated } = await applyPromotions(client, cart.id, [code.trim()])
|
|
90
|
+
onCartChange?.(updated)
|
|
91
|
+
setCode("")
|
|
92
|
+
} catch (e: unknown) {
|
|
93
|
+
// Never surface the raw API error string to the shopper — map to a
|
|
94
|
+
// proper localized message (code-first, pattern fallback).
|
|
95
|
+
setError(translatePromotionError(e, labels.promotionErrors, { hasEmail: !!cart.email }))
|
|
96
|
+
} finally {
|
|
97
|
+
setLoading(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={() => setOpen(!open)}
|
|
106
|
+
className="flex items-center gap-3 w-full p-4 rounded-xl border border-border bg-muted/50 hover:bg-muted transition-colors"
|
|
107
|
+
>
|
|
108
|
+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-warning to-warning/70 flex items-center justify-center flex-shrink-0">
|
|
109
|
+
<svg
|
|
110
|
+
className="w-5 h-5 text-card"
|
|
111
|
+
fill="none"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth={2}
|
|
115
|
+
>
|
|
116
|
+
<path
|
|
117
|
+
strokeLinecap="round"
|
|
118
|
+
strokeLinejoin="round"
|
|
119
|
+
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
|
120
|
+
/>
|
|
121
|
+
<path
|
|
122
|
+
strokeLinecap="round"
|
|
123
|
+
strokeLinejoin="round"
|
|
124
|
+
d="M6 6h.008v.008H6V6z"
|
|
125
|
+
/>
|
|
126
|
+
</svg>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex-1 text-left">
|
|
129
|
+
<p className="text-sm font-semibold text-foreground">
|
|
130
|
+
{labels.discountCode}
|
|
131
|
+
</p>
|
|
132
|
+
<p className="text-xs text-muted-foreground">{labels.discountSub}</p>
|
|
133
|
+
</div>
|
|
134
|
+
<span className="text-sm font-medium text-foreground flex items-center gap-1.5">
|
|
135
|
+
{labels.addCode}
|
|
136
|
+
<svg
|
|
137
|
+
className={cn(
|
|
138
|
+
"w-4 h-4 text-muted-foreground transition-transform duration-200",
|
|
139
|
+
open && "rotate-180"
|
|
140
|
+
)}
|
|
141
|
+
fill="none"
|
|
142
|
+
viewBox="0 0 24 24"
|
|
143
|
+
stroke="currentColor"
|
|
144
|
+
strokeWidth={2}
|
|
145
|
+
>
|
|
146
|
+
<path
|
|
147
|
+
strokeLinecap="round"
|
|
148
|
+
strokeLinejoin="round"
|
|
149
|
+
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
150
|
+
/>
|
|
151
|
+
</svg>
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
|
|
155
|
+
{open && (
|
|
156
|
+
<div className="mt-3 flex gap-2">
|
|
157
|
+
<input
|
|
158
|
+
type="text"
|
|
159
|
+
value={code}
|
|
160
|
+
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
161
|
+
placeholder="PROMO2024"
|
|
162
|
+
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"
|
|
163
|
+
/>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={handleApply}
|
|
167
|
+
disabled={loading || !code.trim()}
|
|
168
|
+
className="px-5 h-11 text-sm font-semibold bg-foreground text-card rounded-xl hover:bg-foreground/90 transition-all disabled:opacity-40"
|
|
169
|
+
>
|
|
170
|
+
{labels.addCode}
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
|
|
175
|
+
{error && <p className="text-xs text-destructive mt-2">{error}</p>}
|
|
176
|
+
|
|
177
|
+
{promotions.length > 0 && (
|
|
178
|
+
<div className="mt-3 space-y-1.5">
|
|
179
|
+
{promotions.map((p) => (
|
|
180
|
+
<div
|
|
181
|
+
key={p.id}
|
|
182
|
+
className="flex items-center justify-between px-4 py-2.5 bg-success/10 border border-success/20 rounded-lg"
|
|
183
|
+
>
|
|
184
|
+
<div className="flex items-center gap-2">
|
|
185
|
+
<svg
|
|
186
|
+
className="w-4 h-4 text-success"
|
|
187
|
+
fill="none"
|
|
188
|
+
viewBox="0 0 24 24"
|
|
189
|
+
stroke="currentColor"
|
|
190
|
+
strokeWidth={2}
|
|
191
|
+
>
|
|
192
|
+
<path
|
|
193
|
+
strokeLinecap="round"
|
|
194
|
+
strokeLinejoin="round"
|
|
195
|
+
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
196
|
+
/>
|
|
197
|
+
</svg>
|
|
198
|
+
<span className="text-sm font-semibold text-success">
|
|
199
|
+
{p.code}
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
<span className="text-sm font-medium text-success">
|
|
203
|
+
{p.application_method?.type === "percentage"
|
|
204
|
+
? `-${p.application_method.value}%`
|
|
205
|
+
: p.application_method?.value && p.application_method?.currency_code
|
|
206
|
+
? `-${convertToLocale({
|
|
207
|
+
amount: +p.application_method.value,
|
|
208
|
+
currency_code: p.application_method.currency_code,
|
|
209
|
+
})}`
|
|
210
|
+
: ""}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
218
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GiftCardErrorCopy,
|
|
3
|
+
PaymentErrorCopy,
|
|
4
|
+
PromotionErrorCopy,
|
|
5
|
+
} from "./labels"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* WHICH COPY AN API ERROR CODE ASKS FOR — the recognition half of the
|
|
9
|
+
* checkout's failure copy, kept apart from the words themselves.
|
|
10
|
+
*
|
|
11
|
+
* The codes are the store API's own (docs/storefront/checkout.md lists
|
|
12
|
+
* every code per endpoint). They are wire format, never shown to a
|
|
13
|
+
* shopper, so they are never translated. The sentence each one maps to
|
|
14
|
+
* lives in the checkout label pack, which means it is English by default
|
|
15
|
+
* and translated by whichever locale the store mounts.
|
|
16
|
+
*
|
|
17
|
+
* Generated 2026-09-13 from the modules' own Bulgarian maps when the copy
|
|
18
|
+
* was lifted; a new code is one line here plus one key in the packs, and
|
|
19
|
+
* the type makes a missing key a compile error.
|
|
20
|
+
*/
|
|
21
|
+
export const PAYMENT_CODE_KEYS: Record<string, keyof PaymentErrorCopy> = {
|
|
22
|
+
cart_email_required: "cartEmailRequired",
|
|
23
|
+
cart_empty: "cartEmpty",
|
|
24
|
+
shipping_address_required: "shippingAddressRequired",
|
|
25
|
+
shipping_method_required: "shippingMethodRequired",
|
|
26
|
+
payment_collection_required: "paymentCollectionRequired",
|
|
27
|
+
payment_session_required: "paymentSessionRequired",
|
|
28
|
+
insufficient_inventory: "insufficientInventory",
|
|
29
|
+
checkout_method_hidden: "checkoutMethodHidden",
|
|
30
|
+
requires_action: "requiresAction",
|
|
31
|
+
payment_not_authorized: "paymentNotAuthorized",
|
|
32
|
+
payment_not_initiated: "paymentNotInitiated",
|
|
33
|
+
payment_incomplete: "paymentIncomplete",
|
|
34
|
+
gift_card_insufficient_balance: "giftCardInsufficientBalance",
|
|
35
|
+
gift_card_not_redeemable: "giftCardNotRedeemable",
|
|
36
|
+
account_required: "accountRequired",
|
|
37
|
+
cart_locked: "cartLocked",
|
|
38
|
+
cart_completed: "cartCompleted",
|
|
39
|
+
cart_not_found: "cartNotFound",
|
|
40
|
+
shipping_option_not_found: "shippingOptionNotFound",
|
|
41
|
+
invalid_provider: "invalidProvider",
|
|
42
|
+
shipping_price_missing: "shippingPriceMissing",
|
|
43
|
+
stripe_not_configured: "stripeNotConfigured",
|
|
44
|
+
validation_failed: "validationFailed",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const GIFT_CARD_CODE_KEYS: Record<string, keyof GiftCardErrorCopy> = {
|
|
48
|
+
invalid_gift_card: "invalidGiftCard",
|
|
49
|
+
rate_limited: "rateLimited",
|
|
50
|
+
cart_completed: "cartCompleted",
|
|
51
|
+
cart_not_found: "cartNotFound",
|
|
52
|
+
validation_failed: "validationFailed",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const PROMOTION_CODE_KEYS: Record<string, keyof PromotionErrorCopy> = {
|
|
56
|
+
promotion_not_found: "promotionNotFound",
|
|
57
|
+
promotion_inactive: "promotionInactive",
|
|
58
|
+
promotion_misconfigured: "promotionMisconfigured",
|
|
59
|
+
promotion_unsupported_type: "promotionUnsupportedType",
|
|
60
|
+
cart_completed: "cartCompleted",
|
|
61
|
+
cart_not_found: "cartNotFound",
|
|
62
|
+
validation_failed: "validationFailed",
|
|
63
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useState } from "react"
|
|
|
5
5
|
import type { StorefrontClient } from "../api/http"
|
|
6
6
|
import type { Cart } from "../api/carts"
|
|
7
7
|
import { applyGiftCard, removeGiftCard } from "../api/gift-cards"
|
|
8
|
-
import {
|
|
8
|
+
import { Price } from "../lib/price"
|
|
9
9
|
import { cn } from "../lib/utils"
|
|
10
10
|
import { useCheckoutLabels } from "./context"
|
|
11
11
|
import { translateGiftCardError } from "./payment-error-copy"
|
|
@@ -74,7 +74,7 @@ export function GiftCardSection({
|
|
|
74
74
|
setCode("")
|
|
75
75
|
} catch (e: unknown) {
|
|
76
76
|
// Generic-oracle discipline: one clean message per code family.
|
|
77
|
-
setError(translateGiftCardError(e))
|
|
77
|
+
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
78
78
|
} finally {
|
|
79
79
|
setLoading(false)
|
|
80
80
|
}
|
|
@@ -89,7 +89,7 @@ export function GiftCardSection({
|
|
|
89
89
|
})
|
|
90
90
|
onCartChange?.(updated)
|
|
91
91
|
} catch (e: unknown) {
|
|
92
|
-
setError(translateGiftCardError(e))
|
|
92
|
+
setError(translateGiftCardError(e, labels.giftCardErrors))
|
|
93
93
|
} finally {
|
|
94
94
|
setRemovingId(null)
|
|
95
95
|
}
|
|
@@ -201,7 +201,7 @@ export function GiftCardSection({
|
|
|
201
201
|
remainder didn't shrink. */}
|
|
202
202
|
<span className="text-sm font-medium text-success">
|
|
203
203
|
-
|
|
204
|
-
<
|
|
204
|
+
<Price
|
|
205
205
|
amount={gc.amount}
|
|
206
206
|
currencyCode={cart.currency_code}
|
|
207
207
|
/>
|
package/src/checkout/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ export {
|
|
|
13
13
|
useOrderConfirmedPath,
|
|
14
14
|
} from "./context"
|
|
15
15
|
export { defaultCheckoutLabels, type CheckoutLabels } from "./labels"
|
|
16
|
-
export { bulgarianCheckoutLabels } from "./labels-bg"
|
|
17
16
|
export { ErrorMessage } from "./error-message"
|
|
18
17
|
export {
|
|
19
18
|
StripeContext,
|
|
@@ -51,16 +50,16 @@ export {
|
|
|
51
50
|
export {
|
|
52
51
|
translatePaymentError,
|
|
53
52
|
translateGiftCardError,
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
paymentErrorKey,
|
|
54
|
+
giftCardErrorKey,
|
|
56
55
|
} from "./payment-error-copy"
|
|
57
56
|
export {
|
|
58
57
|
translateAddressError,
|
|
59
|
-
|
|
58
|
+
addressErrorKey,
|
|
60
59
|
} from "./address-error-copy"
|
|
61
60
|
export {
|
|
62
61
|
translatePromotionError,
|
|
63
|
-
|
|
62
|
+
promotionErrorKey,
|
|
64
63
|
} from "./promotion-error-copy"
|
|
65
64
|
export { default as compareAddresses } from "./compare-addresses"
|
|
66
65
|
export {
|