@cartbase/storefront 0.9.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 +6 -7
- package/src/cart-drawer/index.ts +0 -1
- 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 +2 -2
- package/src/checkout/index.ts +4 -5
- package/src/checkout/labels.ts +218 -0
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -269
- package/src/checkout/promotion-error-copy.ts +91 -124
- package/src/checkout/use-checkout-orchestration.ts +10 -8
- 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/products/index.ts +0 -1
- package/src/reviews-ui/index.ts +0 -1
- package/src/store/index.ts +0 -1
- package/src/tracking/consent-banner.tsx +5 -1
- package/src/cart-drawer/labels-bg.ts +0 -72
- package/src/checkout/labels-bg.ts +0 -128
- 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,269 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Processing error
|
|
156
|
-
{
|
|
157
|
-
match: /processing[\s_]?error/i,
|
|
158
|
-
copy: "Грешка при обработка на картата. Моля, опитайте отново след малко.",
|
|
159
|
-
},
|
|
160
|
-
// Rate limit
|
|
161
|
-
{
|
|
162
|
-
match: /rate[\s_]?limit/i,
|
|
163
|
-
copy: "Твърде много опити за плащане. Моля, изчакайте няколко минути и опитайте отново.",
|
|
164
|
-
},
|
|
165
|
-
// Terminal-state PI (the bug class refresh-payment-if-terminal heals)
|
|
166
|
-
{
|
|
167
|
-
match: /terminal[\s_]?state|payment[\s_]?intent[\s_]?(?:is|in)[\s_]?(?:a[\s_]?)?terminal/i,
|
|
168
|
-
copy: "Сесията за плащане е изтекла. Моля, презаредете страницата и опитайте отново.",
|
|
169
|
-
},
|
|
170
|
-
// Amount mismatch — happens when cart total drifts from PI amount
|
|
171
|
-
{
|
|
172
|
-
match: /amount[\s_]?mismatch|amount[\s_]?does[\s_]?not[\s_]?match/i,
|
|
173
|
-
copy: "Сумата на поръчката се промени. Моля, презаредете страницата и опитайте отново.",
|
|
174
|
-
},
|
|
175
|
-
// Network / fetch failure
|
|
176
|
-
{
|
|
177
|
-
match: /failed[\s_]?to[\s_]?fetch|network[\s_]?error|networkerror/i,
|
|
178
|
-
copy: "Няма връзка със сървъра. Моля, проверете интернет връзката и опитайте отново.",
|
|
179
|
-
},
|
|
180
|
-
]
|
|
181
|
-
|
|
182
|
-
/** Legacy Medusa-era API substrings — kept as the last fallback layer. */
|
|
183
|
-
const API_PATTERNS: Array<{ match: RegExp; copy: string }> = [
|
|
184
|
-
// Cart already completed — likely a double-submit racing through
|
|
185
|
-
{
|
|
186
|
-
match: /cart[\s_]?(?:is[\s_]?)?already[\s_]?completed|already[\s_]?an[\s_]?order/i,
|
|
187
|
-
copy: "Тази поръчка вече беше финализирана. Моля, проверете имейла си за потвърждение.",
|
|
188
|
-
},
|
|
189
|
-
// Out of stock
|
|
190
|
-
{
|
|
191
|
-
match: /not[\s_]?enough[\s_]?stock|insufficient[\s_]?stock|out[\s_]?of[\s_]?stock|insufficient[\s_]?inventory/i,
|
|
192
|
-
copy: "Един от продуктите вече не е наличен в избраното количество. Моля, обновете количката.",
|
|
193
|
-
},
|
|
194
|
-
// Region / country mismatch
|
|
195
|
-
{
|
|
196
|
-
match: /no[\s_]?region|invalid[\s_]?region/i,
|
|
197
|
-
copy: "Регионът на доставка не е валиден. Моля, презаредете страницата.",
|
|
198
|
-
},
|
|
199
|
-
// No shipping method
|
|
200
|
-
{
|
|
201
|
-
match: /no[\s_]?shipping[\s_]?method|shipping[\s_]?method[\s_]?not[\s_]?found/i,
|
|
202
|
-
copy: "Изберете метод на доставка преди да продължите.",
|
|
203
|
-
},
|
|
204
|
-
]
|
|
205
|
-
|
|
206
|
-
/** Pull the structured Cartbase error code out of any thrown value. */
|
|
207
|
-
function extractCode(err: unknown): string | null {
|
|
208
|
-
if (err && typeof err === "object") {
|
|
209
|
-
const code = (err as { code?: unknown }).code
|
|
210
|
-
if (typeof code === "string" && code.length > 0) return code
|
|
211
|
-
}
|
|
212
|
-
return null
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Translate any thrown / returned error into customer-facing Bulgarian.
|
|
217
|
-
* Code-first (Cartbase `StoreApiError.code`), then Stripe substring patterns,
|
|
218
|
-
* then legacy API substrings, then the per-context generic.
|
|
219
|
-
*
|
|
220
|
-
* @param err - the error from a try/catch or a Stripe error response
|
|
221
|
-
* @param context - "card" for Stripe path, "cod" for manual path
|
|
222
|
-
*/
|
|
223
|
-
export function translatePaymentError(
|
|
224
|
-
err: unknown,
|
|
225
|
-
context: ErrorContext
|
|
226
|
-
): string {
|
|
227
|
-
const code = extractCode(err)
|
|
228
|
-
if (code && PAYMENT_ERROR_CODE_COPY[code]) return PAYMENT_ERROR_CODE_COPY[code]
|
|
229
|
-
|
|
230
|
-
const raw = extractMessage(err)
|
|
231
|
-
if (!raw) return context === "card" ? GENERIC_CARD : GENERIC_COD
|
|
232
|
-
|
|
233
|
-
for (const { match, copy } of STRIPE_PATTERNS) {
|
|
234
|
-
if (match.test(raw)) return copy
|
|
235
|
-
}
|
|
236
|
-
for (const { match, copy } of API_PATTERNS) {
|
|
237
|
-
if (match.test(raw)) return copy
|
|
238
|
-
}
|
|
239
|
-
return context === "card" ? GENERIC_CARD : GENERIC_COD
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Translate a gift-card apply/remove failure into customer-facing Bulgarian.
|
|
244
|
-
* Consumed by `GiftCardSection`. Code-first; falls back to one clean generic
|
|
245
|
-
* (the API's anti-oracle design means there is nothing more specific to say).
|
|
246
|
-
*/
|
|
247
|
-
export function translateGiftCardError(err: unknown): string {
|
|
248
|
-
const code = extractCode(err)
|
|
249
|
-
if (code && GIFT_CARD_ERROR_CODE_COPY[code]) return GIFT_CARD_ERROR_CODE_COPY[code]
|
|
250
|
-
return GIFT_CARD_GENERIC
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function extractMessage(err: unknown): string {
|
|
254
|
-
if (!err) return ""
|
|
255
|
-
if (typeof err === "string") return err
|
|
256
|
-
if (err instanceof Error) return err.message
|
|
257
|
-
if (typeof err === "object" && err !== null) {
|
|
258
|
-
const e = err as {
|
|
259
|
-
message?: string
|
|
260
|
-
code?: string
|
|
261
|
-
decline_code?: string
|
|
262
|
-
type?: string
|
|
263
|
-
}
|
|
264
|
-
return [e.message, e.code, e.decline_code, e.type]
|
|
265
|
-
.filter(Boolean)
|
|
266
|
-
.join(" ")
|
|
267
|
-
}
|
|
268
|
-
return String(err)
|
|
269
|
-
}
|
|
1
|
+
import { GIFT_CARD_CODE_KEYS, PAYMENT_CODE_KEYS } from "./error-copy-codes"
|
|
2
|
+
import type { GiftCardErrorCopy, PaymentErrorCopy } from "./labels"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recognizes a payment or order-completion failure and picks the copy key
|
|
6
|
+
* for it.
|
|
7
|
+
*
|
|
8
|
+
* THE COPY IS NOT HERE ANY MORE. Until 2026-09-13 this file held 47
|
|
9
|
+
* hardcoded BULGARIAN sentences, so every merchant who installed the
|
|
10
|
+
* package showed Bulgarian at the exact moment a payment failed, with no
|
|
11
|
+
* override. The words now live in the checkout label pack: English by
|
|
12
|
+
* default, translated by whichever locale the store mounts.
|
|
13
|
+
*
|
|
14
|
+
* WHY THIS LAYER EXISTS AT ALL: Stripe and API errors are technical
|
|
15
|
+
* English ("Your card's security code is incorrect.", "fetch failed").
|
|
16
|
+
* Showing that to a shopper makes the site look broken rather than making
|
|
17
|
+
* it look like they mistyped a digit. Unknown errors get the per-context
|
|
18
|
+
* generic, because a half-translated technical string is worse than a
|
|
19
|
+
* clean one.
|
|
20
|
+
*
|
|
21
|
+
* Recognition is CODE-FIRST — the store API throws `StoreApiError` with a
|
|
22
|
+
* structured `code` (docs/storefront/checkout.md lists every code per
|
|
23
|
+
* endpoint) — then the Stripe substring patterns, which still do the heavy
|
|
24
|
+
* lifting on the card path because Stripe.js browser errors carry no
|
|
25
|
+
* Cartbase code, then the legacy Medusa-era substrings.
|
|
26
|
+
*
|
|
27
|
+
* The patterns stay in English on purpose: they match the wire, not the
|
|
28
|
+
* shopper.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type ErrorContext = "card" | "cod"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Stripe error codes, decline codes and common substrings. Matched
|
|
35
|
+
* case-insensitively, first match wins, so the order runs most specific to
|
|
36
|
+
* most generic. Kept verbatim from the production port.
|
|
37
|
+
*
|
|
38
|
+
* Stripe error code reference: https://docs.stripe.com/error-codes
|
|
39
|
+
*/
|
|
40
|
+
const STRIPE_PATTERNS: Array<{ match: RegExp; key: keyof PaymentErrorCopy }> = [
|
|
41
|
+
{ match: /card[\s_-]?declined|card_declined/i, key: "cardDeclined" },
|
|
42
|
+
{ match: /insufficient[\s_]?funds/i, key: "insufficientFunds" },
|
|
43
|
+
{ match: /expired[\s_]?card|card[\s_]?has[\s_]?expired/i, key: "expiredCard" },
|
|
44
|
+
{ match: /incorrect[\s_]?cvc|invalid[\s_]?cvc|cvc[\s_]?check/i, key: "incorrectCvc" },
|
|
45
|
+
{ match: /incorrect[\s_]?number|invalid[\s_]?number/i, key: "incorrectCardNumber" },
|
|
46
|
+
{ match: /invalid[\s_]?expir|incorrect[\s_]?expir/i, key: "invalidExpiry" },
|
|
47
|
+
{ match: /authentication[\s_]?required|3d[\s_]?secure/i, key: "authenticationRequired" },
|
|
48
|
+
{ match: /processing[\s_]?error/i, key: "processingError" },
|
|
49
|
+
{ match: /rate[\s_]?limit/i, key: "rateLimited" },
|
|
50
|
+
// Terminal-state payment intent: the bug class refresh-payment-if-terminal heals.
|
|
51
|
+
{
|
|
52
|
+
match: /terminal[\s_]?state|payment[\s_]?intent[\s_]?(?:is|in)[\s_]?(?:a[\s_]?)?terminal/i,
|
|
53
|
+
key: "paymentExpired",
|
|
54
|
+
},
|
|
55
|
+
// Amount mismatch: the cart total drifted from the payment intent's amount.
|
|
56
|
+
{ match: /amount[\s_]?mismatch|amount[\s_]?does[\s_]?not[\s_]?match/i, key: "amountMismatch" },
|
|
57
|
+
{ match: /failed[\s_]?to[\s_]?fetch|network[\s_]?error|networkerror/i, key: "networkError" },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** Legacy Medusa-era API substrings, the last fallback layer. */
|
|
61
|
+
const API_PATTERNS: Array<{ match: RegExp; key: keyof PaymentErrorCopy }> = [
|
|
62
|
+
{
|
|
63
|
+
match: /cart[\s_]?(?:is[\s_]?)?already[\s_]?completed|already[\s_]?an[\s_]?order/i,
|
|
64
|
+
key: "cartAlreadyCompleted",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
match:
|
|
68
|
+
/not[\s_]?enough[\s_]?stock|insufficient[\s_]?stock|out[\s_]?of[\s_]?stock|insufficient[\s_]?inventory/i,
|
|
69
|
+
key: "outOfStock",
|
|
70
|
+
},
|
|
71
|
+
{ match: /no[\s_]?region|invalid[\s_]?region/i, key: "invalidRegion" },
|
|
72
|
+
{
|
|
73
|
+
match: /no[\s_]?shipping[\s_]?method|shipping[\s_]?method[\s_]?not[\s_]?found/i,
|
|
74
|
+
key: "shippingMethodMissing",
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
/** Pull the structured Cartbase error code out of any thrown value. */
|
|
79
|
+
function extractCode(err: unknown): string | null {
|
|
80
|
+
if (err && typeof err === "object") {
|
|
81
|
+
const code = (err as { code?: unknown }).code
|
|
82
|
+
if (typeof code === "string" && code.length > 0) return code
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Which copy this failure asks for, independent of language. */
|
|
88
|
+
export function paymentErrorKey(
|
|
89
|
+
err: unknown,
|
|
90
|
+
context: ErrorContext
|
|
91
|
+
): keyof PaymentErrorCopy {
|
|
92
|
+
const generic = context === "card" ? "genericCard" : "genericCod"
|
|
93
|
+
|
|
94
|
+
const code = extractCode(err)
|
|
95
|
+
if (code && PAYMENT_CODE_KEYS[code]) return PAYMENT_CODE_KEYS[code]
|
|
96
|
+
|
|
97
|
+
const raw = extractMessage(err)
|
|
98
|
+
if (!raw) return generic
|
|
99
|
+
|
|
100
|
+
for (const { match, key } of STRIPE_PATTERNS) if (match.test(raw)) return key
|
|
101
|
+
for (const { match, key } of API_PATTERNS) if (match.test(raw)) return key
|
|
102
|
+
return generic
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Turn any thrown or returned payment error into the sentence a shopper
|
|
107
|
+
* reads, in the store's own language.
|
|
108
|
+
*
|
|
109
|
+
* @param err - the error from a try/catch or a Stripe error response
|
|
110
|
+
* @param context - "card" for the Stripe path, "cod" for the manual path
|
|
111
|
+
* @param copy - the store's payment copy (`labels.paymentErrors`)
|
|
112
|
+
*/
|
|
113
|
+
export function translatePaymentError(
|
|
114
|
+
err: unknown,
|
|
115
|
+
context: ErrorContext,
|
|
116
|
+
copy: PaymentErrorCopy
|
|
117
|
+
): string {
|
|
118
|
+
return copy[paymentErrorKey(err, context)]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Which gift-card copy this failure asks for. */
|
|
122
|
+
export function giftCardErrorKey(err: unknown): keyof GiftCardErrorCopy {
|
|
123
|
+
const code = extractCode(err)
|
|
124
|
+
if (code && GIFT_CARD_CODE_KEYS[code]) return GIFT_CARD_CODE_KEYS[code]
|
|
125
|
+
return "generic"
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Turn a gift-card apply or remove failure into the shopper's sentence.
|
|
130
|
+
* Code-first, falling back to one clean generic: the API's anti-oracle
|
|
131
|
+
* design means there is deliberately nothing more specific to say.
|
|
132
|
+
*/
|
|
133
|
+
export function translateGiftCardError(
|
|
134
|
+
err: unknown,
|
|
135
|
+
copy: GiftCardErrorCopy
|
|
136
|
+
): string {
|
|
137
|
+
return copy[giftCardErrorKey(err)]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractMessage(err: unknown): string {
|
|
141
|
+
if (!err) return ""
|
|
142
|
+
if (typeof err === "string") return err
|
|
143
|
+
if (err instanceof Error) return err.message
|
|
144
|
+
if (typeof err === "object" && err !== null) {
|
|
145
|
+
const e = err as {
|
|
146
|
+
message?: string
|
|
147
|
+
code?: string
|
|
148
|
+
decline_code?: string
|
|
149
|
+
type?: string
|
|
150
|
+
}
|
|
151
|
+
return [e.message, e.code, e.decline_code, e.type].filter(Boolean).join(" ")
|
|
152
|
+
}
|
|
153
|
+
return String(err)
|
|
154
|
+
}
|