@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.
Files changed (50) hide show
  1. package/package.json +7 -8
  2. package/src/cart-drawer/cross-sell-carousel.tsx +2 -3
  3. package/src/cart-drawer/gift-wrap.tsx +82 -83
  4. package/src/cart-drawer/index.ts +0 -1
  5. package/src/cart-drawer/item/index.tsx +4 -6
  6. package/src/cart-drawer/sticky-footer.tsx +73 -73
  7. package/src/checkout/address-error-copy.ts +117 -119
  8. package/src/checkout/discount-section.tsx +218 -218
  9. package/src/checkout/error-copy-codes.ts +63 -0
  10. package/src/checkout/gift-card-section.tsx +4 -4
  11. package/src/checkout/index.ts +4 -5
  12. package/src/checkout/labels.ts +218 -0
  13. package/src/checkout/line-item-card.tsx +3 -3
  14. package/src/checkout/order-summary.tsx +11 -12
  15. package/src/checkout/payment-button.tsx +372 -373
  16. package/src/checkout/payment-error-copy.ts +154 -269
  17. package/src/checkout/promotion-error-copy.ts +91 -124
  18. package/src/checkout/shipping-method-list.tsx +2 -2
  19. package/src/checkout/use-checkout-orchestration.ts +10 -8
  20. package/src/lib/money.ts +1 -1
  21. package/src/lib/price.tsx +39 -0
  22. package/src/locales/bg.ts +468 -0
  23. package/src/locales/en.ts +26 -0
  24. package/src/locales/es.ts +467 -0
  25. package/src/locales/index.ts +19 -0
  26. package/src/locales/provider.tsx +63 -0
  27. package/src/locales/types.ts +49 -0
  28. package/src/order/index.ts +0 -1
  29. package/src/order/order-delivery-card.tsx +2 -2
  30. package/src/order/order-item.tsx +3 -3
  31. package/src/order/order-totals.tsx +4 -4
  32. package/src/products/index.ts +0 -1
  33. package/src/reviews-ui/index.ts +0 -1
  34. package/src/store/index.ts +0 -1
  35. package/src/tracking/attribution.ts +13 -0
  36. package/src/tracking/chatgpt-pixel.tsx +92 -0
  37. package/src/tracking/consent-banner.tsx +5 -1
  38. package/src/tracking/consent.ts +12 -0
  39. package/src/tracking/events.ts +56 -2
  40. package/src/tracking/index.ts +12 -0
  41. package/src/tracking/oaiq.ts +206 -0
  42. package/src/tracking/storefront-tags.tsx +2 -0
  43. package/src/tracking/types.ts +4 -0
  44. package/src/cart-drawer/labels-bg.ts +0 -72
  45. package/src/checkout/labels-bg.ts +0 -128
  46. package/src/lib/dual-price.tsx +0 -73
  47. package/src/order/labels-bg.ts +0 -39
  48. package/src/products/labels-bg.ts +0 -35
  49. package/src/reviews-ui/labels-bg.ts +0 -91
  50. package/src/store/labels-bg.ts +0 -22
@@ -1,269 +1,154 @@
1
- /**
2
- * Translates payment / order-completion errors into user-friendly
3
- * Bulgarian copy for the checkout payment surface.
4
- *
5
- * Ported from `@1click/ui/src/checkout/payment-error-copy.ts` (v2.3.1) and
6
- * adapted to the Cartbase error envelope: the store API throws `StoreApiError`
7
- * with a STRUCTURED `code` (docs/storefront/checkout.md lists every code per
8
- * endpoint), so translation is **code-first** the substring patterns from
9
- * the Medusa era are kept as the fallback layer for Stripe.js browser errors
10
- * (which have no Cartbase code) and any legacy wire.
11
- *
12
- * Why this exists: Stripe and API errors are technical English strings
13
- * ("Your card's security code is incorrect.", "fetch failed"). Showing them
14
- * to a Bulgarian shopper at the moment of payment failure is the worst UX
15
- * surface possible it looks like the site is broken, not like the customer
16
- * made a typo. Unknown errors get the generic fallback showing a
17
- * half-translated message is worse than a clean fallback.
18
- */
19
-
20
- type ErrorContext = "card" | "cod"
21
-
22
- const GENERIC_CARD =
23
- "Плащането не може да бъде обработено в момента. Моля, опитайте отново или изберете друг метод на плащане."
24
-
25
- const GENERIC_COD =
26
- "Поръчката не може да бъде финализирана в момента. Моля, опитайте отново след малко."
27
-
28
- /**
29
- * Cartbase store-API error codes → BG copy. Covers EVERY code documented for
30
- * POST /api/store/carts/:id/complete, prepare-checkout,
31
- * sync-payment-amount and refresh-payment-if-terminal
32
- * (docs/storefront/checkout.md + carts.completeCart JSDoc). Code-first:
33
- * matched before any substring pattern.
34
- */
35
- export const PAYMENT_ERROR_CODE_COPY: Record<string, string> = {
36
- // complete: validation family (400)
37
- cart_email_required:
38
- "Въведете имейл адрес, за да завършите поръчката.",
39
- cart_empty: "Количката е празна. Добавете продукти, за да продължите.",
40
- shipping_address_required:
41
- "Въведете адрес за доставка, за да завършите поръчката.",
42
- shipping_method_required:
43
- "Изберете метод на доставка преди да продължите.",
44
- payment_collection_required:
45
- "Плащането не е подготвено. Моля, опитайте отново.",
46
- payment_session_required:
47
- "Плащането не е подготвено. Моля, опитайте отново.",
48
- insufficient_inventory:
49
- "Един от продуктите вече не е наличен в избраното количество. Моля, обновете количката.",
50
- // — checkout-rules security boundary (400)
51
- checkout_method_hidden:
52
- "Избраният метод вече не е наличен за тази поръчка. Моля, изберете друг метод и опитайте отново.",
53
- // — payment family (402) —
54
- requires_action:
55
- "Банката изисква допълнително потвърждение. Моля, следвайте инструкциите.",
56
- payment_not_authorized:
57
- "Плащането не беше одобрено. Моля, опитайте отново или изберете друг метод.",
58
- payment_not_initiated:
59
- "Сесията за плащане е изтекла. Моля, презаредете страницата и опитайте отново.",
60
- payment_incomplete:
61
- "Картите за подарък вече не покриват цялата сума. Моля, изберете начин на плащане за остатъка.",
62
- gift_card_insufficient_balance:
63
- "Наличността по картата за подарък вече не е достатъчна. Моля, премахнете я или изберете друг начин на плащане.",
64
- gift_card_not_redeemable:
65
- "Картата за подарък вече не може да бъде използвана. Моля, премахнете я и опитайте отново.",
66
- // — access / concurrency —
67
- account_required:
68
- "За да завършите поръчката, е необходимо да влезете в профила си.",
69
- cart_locked:
70
- "Поръчката се обработва в момента. Моля, изчакайте няколко секунди.",
71
- cart_completed:
72
- "Тази поръчка вече беше финализирана. Моля, проверете имейла си за потвърждение.",
73
- cart_not_found: "Сесията на количката изтече. Моля, презаредете страницата.",
74
- // — prepare-checkout family —
75
- shipping_option_not_found:
76
- "Избраният метод на доставка вече не е наличен. Моля, изберете друг.",
77
- invalid_provider:
78
- "Избраният начин на плащане не е валиден. Моля, изберете друг метод.",
79
- shipping_price_missing:
80
- "Цената за доставка не може да бъде определена. Моля, изберете друг метод на доставка.",
81
- stripe_not_configured:
82
- "Плащането с карта временно не е налично. Моля, изберете друг метод.",
83
- validation_failed:
84
- "Данните за поръчката са непълни. Моля, проверете формата и опитайте отново.",
85
- }
86
-
87
- /**
88
- * Gift-card apply/remove codes → BG copy (POST/DELETE
89
- * /api/store/carts/:id/gift-cards — docs/storefront/gift-cards.md).
90
- * The API deliberately answers unknown/disabled/expired/depleted/foreign
91
- * codes with ONE generic `invalid_gift_card` (no code-existence oracle) —
92
- * the copy mirrors that: one honest generic message.
93
- */
94
- export const GIFT_CARD_ERROR_CODE_COPY: Record<string, string> = {
95
- invalid_gift_card:
96
- "Кодът на картата за подарък не е валиден. Моля, проверете изписването.",
97
- rate_limited:
98
- "Твърде много опити. Моля, изчакайте няколко минути и опитайте отново.",
99
- cart_completed:
100
- "Поръчката вече е финализирана. Моля, проверете имейла си за потвърждение.",
101
- cart_not_found: "Сесията на количката изтече. Моля, презаредете страницата.",
102
- validation_failed:
103
- "Невалиден код. Моля, проверете изписването и опитайте отново.",
104
- }
105
-
106
- const GIFT_CARD_GENERIC =
107
- "Картата за подарък не може да бъде приложена в момента. Моля, опитайте отново."
108
-
109
- /**
110
- * Map of Stripe error codes / decline codes / common substrings to BG copy.
111
- * Substring match is case-insensitive. First match wins, so order from
112
- * most-specific to most-generic. Kept verbatim from the production port —
113
- * Stripe.js browser errors carry no Cartbase `code`, so this layer still does
114
- * the heavy lifting on the card path.
115
- *
116
- * Stripe error code reference:
117
- * https://docs.stripe.com/error-codes
118
- */
119
- const STRIPE_PATTERNS: Array<{ match: RegExp; copy: string }> = [
120
- // Card declined — generic
121
- {
122
- match: /card[\s_-]?declined|card_declined/i,
123
- copy: "Картата е отказана от банката. Моля, опитайте с друга карта или се свържете с банката си.",
124
- },
125
- // Insufficient funds
126
- {
127
- match: /insufficient[\s_]?funds/i,
128
- copy: "Недостатъчна наличност по картата. Моля, опитайте с друга карта.",
129
- },
130
- // Expired card
131
- {
132
- match: /expired[\s_]?card|card[\s_]?has[\s_]?expired/i,
133
- copy: "Картата е изтекла. Моля, опитайте с друга карта.",
134
- },
135
- // Wrong CVC / security code
136
- {
137
- match: /incorrect[\s_]?cvc|invalid[\s_]?cvc|cvc[\s_]?check/i,
138
- copy: "Невалиден код за сигурност (CVC). Моля, проверете трите цифри на гърба на картата.",
139
- },
140
- // Wrong card number
141
- {
142
- match: /incorrect[\s_]?number|invalid[\s_]?number/i,
143
- copy: "Невалиден номер на карта. Моля, проверете цифрите.",
144
- },
145
- // Wrong / invalid expiry
146
- {
147
- match: /invalid[\s_]?expir|incorrect[\s_]?expir/i,
148
- copy: "Невалидна дата на изтичане. Моля, проверете месеца и годината.",
149
- },
150
- // 3DS / authentication required
151
- {
152
- match: /authentication[\s_]?required|3d[\s_]?secure/i,
153
- copy: "Банката изисква допълнително потвърждение. Моля, следвайте инструкциите.",
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
+ }