@cartbase/storefront 0.17.0 → 0.18.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/LICENSE +21 -21
- package/package.json +258 -256
- 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/products.ts +43 -1
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/reviews.ts +259 -259
- 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 +277 -164
- 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/index.ts +71 -68
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/quantity.tsx +8 -14
- 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 -0
- 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/checkout/use-checkout-orchestration.ts +1592 -1592
- 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/bg.ts +467 -467
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +466 -466
- 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/index.ts +68 -59
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-actions-wrapper.tsx +63 -62
- package/src/products/product-actions.tsx +151 -150
- 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/product-template.tsx +163 -162
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -0
- package/src/products/use-product-actions.ts +199 -164
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/index.ts +73 -73
- 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
- package/theme/theme.css +150 -150
- package/theme/tokens.css +106 -106
|
@@ -1,154 +1,154 @@
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
import { PROMOTION_CODE_KEYS } from "./error-copy-codes"
|
|
2
|
-
import type { PromotionErrorCopy } from "./labels"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Recognizes a discount-code failure and picks the copy key for it.
|
|
6
|
-
*
|
|
7
|
-
* THE COPY IS NOT HERE ANY MORE. Until 2026-09-13 this file held seven
|
|
8
|
-
* hardcoded BULGARIAN sentences that every merchant shipped and none could
|
|
9
|
-
* override; the words now live in the checkout label pack, English by
|
|
10
|
-
* default. The substring patterns stay in English because they match the
|
|
11
|
-
* API's own error text, which is wire format and never shown.
|
|
12
|
-
*
|
|
13
|
-
* Recognition is code-first (`StoreApiError.code`), then the substrings
|
|
14
|
-
* kept verbatim from the production port.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Known promo error substrings. Case-insensitive, first match wins, most
|
|
19
|
-
* specific first.
|
|
20
|
-
*/
|
|
21
|
-
const PROMO_PATTERNS: Array<{ match: RegExp; key: keyof PromotionErrorCopy }> = [
|
|
22
|
-
// Campaign budget needs an email on the cart.
|
|
23
|
-
{ match: /customer_email|campaign[\s_]?budget/i, key: "needsEmail" },
|
|
24
|
-
// Expired or no-longer-active promotion.
|
|
25
|
-
{ match: /expired|no[\s_]?longer[\s_]?(?:active|valid)|inactive/i, key: "promotionInactive" },
|
|
26
|
-
// Budget or usage limit reached.
|
|
27
|
-
{
|
|
28
|
-
match: /reached[\s_]?its[\s_]?limit|usage[\s_]?limit|budget[\s_]?(?:exceeded|reached)/i,
|
|
29
|
-
key: "exhausted",
|
|
30
|
-
},
|
|
31
|
-
// Invalid or unknown code. Last of the specific set, because it is the
|
|
32
|
-
// broadest pattern and would otherwise swallow the ones above.
|
|
33
|
-
{
|
|
34
|
-
match: /is[\s_]?invalid|invalid[\s_]?data|not[\s_]?found|does(?:n'?t| not)[\s_]?exist/i,
|
|
35
|
-
key: "promotionNotFound",
|
|
36
|
-
},
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Which promotion copy this failure asks for.
|
|
41
|
-
*
|
|
42
|
-
* @param err the error from a try/catch: a StoreApiError with a code, a
|
|
43
|
-
* network error, or an opaque render error
|
|
44
|
-
* @param opts.hasEmail whether the cart currently has an email. When the
|
|
45
|
-
* error is opaque and there is no email, the overwhelmingly
|
|
46
|
-
* likely cause is a campaign-budget promotion, so the shopper
|
|
47
|
-
* gets the actionable "enter your email" line rather than a
|
|
48
|
-
* vague fallback.
|
|
49
|
-
*/
|
|
50
|
-
export function promotionErrorKey(
|
|
51
|
-
err: unknown,
|
|
52
|
-
opts?: { hasEmail?: boolean }
|
|
53
|
-
): keyof PromotionErrorCopy {
|
|
54
|
-
if (err && typeof err === "object") {
|
|
55
|
-
const code = (err as { code?: unknown }).code
|
|
56
|
-
if (typeof code === "string" && PROMOTION_CODE_KEYS[code]) {
|
|
57
|
-
return PROMOTION_CODE_KEYS[code]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const raw = extractMessage(err)
|
|
62
|
-
for (const { match, key } of PROMO_PATTERNS) if (match.test(raw)) return key
|
|
63
|
-
|
|
64
|
-
// Unknown or opaque. With no email on the cart the campaign-budget case
|
|
65
|
-
// is by far the most common trigger, so point at the fix, not a dead end.
|
|
66
|
-
if (opts?.hasEmail === false) return "needsEmail"
|
|
67
|
-
return "generic"
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Turn any thrown or returned promotion error into the sentence a shopper
|
|
72
|
-
* reads, in the store's own language.
|
|
73
|
-
*/
|
|
74
|
-
export function translatePromotionError(
|
|
75
|
-
err: unknown,
|
|
76
|
-
copy: PromotionErrorCopy,
|
|
77
|
-
opts?: { hasEmail?: boolean }
|
|
78
|
-
): string {
|
|
79
|
-
return copy[promotionErrorKey(err, opts)]
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function extractMessage(err: unknown): string {
|
|
83
|
-
if (!err) return ""
|
|
84
|
-
if (typeof err === "string") return err
|
|
85
|
-
if (err instanceof Error) return err.message
|
|
86
|
-
if (typeof err === "object" && err !== null) {
|
|
87
|
-
const e = err as { message?: string; code?: string; type?: string }
|
|
88
|
-
return [e.message, e.code, e.type].filter(Boolean).join(" ")
|
|
89
|
-
}
|
|
90
|
-
return String(err)
|
|
91
|
-
}
|
|
1
|
+
import { PROMOTION_CODE_KEYS } from "./error-copy-codes"
|
|
2
|
+
import type { PromotionErrorCopy } from "./labels"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recognizes a discount-code failure and picks the copy key for it.
|
|
6
|
+
*
|
|
7
|
+
* THE COPY IS NOT HERE ANY MORE. Until 2026-09-13 this file held seven
|
|
8
|
+
* hardcoded BULGARIAN sentences that every merchant shipped and none could
|
|
9
|
+
* override; the words now live in the checkout label pack, English by
|
|
10
|
+
* default. The substring patterns stay in English because they match the
|
|
11
|
+
* API's own error text, which is wire format and never shown.
|
|
12
|
+
*
|
|
13
|
+
* Recognition is code-first (`StoreApiError.code`), then the substrings
|
|
14
|
+
* kept verbatim from the production port.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Known promo error substrings. Case-insensitive, first match wins, most
|
|
19
|
+
* specific first.
|
|
20
|
+
*/
|
|
21
|
+
const PROMO_PATTERNS: Array<{ match: RegExp; key: keyof PromotionErrorCopy }> = [
|
|
22
|
+
// Campaign budget needs an email on the cart.
|
|
23
|
+
{ match: /customer_email|campaign[\s_]?budget/i, key: "needsEmail" },
|
|
24
|
+
// Expired or no-longer-active promotion.
|
|
25
|
+
{ match: /expired|no[\s_]?longer[\s_]?(?:active|valid)|inactive/i, key: "promotionInactive" },
|
|
26
|
+
// Budget or usage limit reached.
|
|
27
|
+
{
|
|
28
|
+
match: /reached[\s_]?its[\s_]?limit|usage[\s_]?limit|budget[\s_]?(?:exceeded|reached)/i,
|
|
29
|
+
key: "exhausted",
|
|
30
|
+
},
|
|
31
|
+
// Invalid or unknown code. Last of the specific set, because it is the
|
|
32
|
+
// broadest pattern and would otherwise swallow the ones above.
|
|
33
|
+
{
|
|
34
|
+
match: /is[\s_]?invalid|invalid[\s_]?data|not[\s_]?found|does(?:n'?t| not)[\s_]?exist/i,
|
|
35
|
+
key: "promotionNotFound",
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Which promotion copy this failure asks for.
|
|
41
|
+
*
|
|
42
|
+
* @param err the error from a try/catch: a StoreApiError with a code, a
|
|
43
|
+
* network error, or an opaque render error
|
|
44
|
+
* @param opts.hasEmail whether the cart currently has an email. When the
|
|
45
|
+
* error is opaque and there is no email, the overwhelmingly
|
|
46
|
+
* likely cause is a campaign-budget promotion, so the shopper
|
|
47
|
+
* gets the actionable "enter your email" line rather than a
|
|
48
|
+
* vague fallback.
|
|
49
|
+
*/
|
|
50
|
+
export function promotionErrorKey(
|
|
51
|
+
err: unknown,
|
|
52
|
+
opts?: { hasEmail?: boolean }
|
|
53
|
+
): keyof PromotionErrorCopy {
|
|
54
|
+
if (err && typeof err === "object") {
|
|
55
|
+
const code = (err as { code?: unknown }).code
|
|
56
|
+
if (typeof code === "string" && PROMOTION_CODE_KEYS[code]) {
|
|
57
|
+
return PROMOTION_CODE_KEYS[code]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const raw = extractMessage(err)
|
|
62
|
+
for (const { match, key } of PROMO_PATTERNS) if (match.test(raw)) return key
|
|
63
|
+
|
|
64
|
+
// Unknown or opaque. With no email on the cart the campaign-budget case
|
|
65
|
+
// is by far the most common trigger, so point at the fix, not a dead end.
|
|
66
|
+
if (opts?.hasEmail === false) return "needsEmail"
|
|
67
|
+
return "generic"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Turn any thrown or returned promotion error into the sentence a shopper
|
|
72
|
+
* reads, in the store's own language.
|
|
73
|
+
*/
|
|
74
|
+
export function translatePromotionError(
|
|
75
|
+
err: unknown,
|
|
76
|
+
copy: PromotionErrorCopy,
|
|
77
|
+
opts?: { hasEmail?: boolean }
|
|
78
|
+
): string {
|
|
79
|
+
return copy[promotionErrorKey(err, opts)]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractMessage(err: unknown): string {
|
|
83
|
+
if (!err) return ""
|
|
84
|
+
if (typeof err === "string") return err
|
|
85
|
+
if (err instanceof Error) return err.message
|
|
86
|
+
if (typeof err === "object" && err !== null) {
|
|
87
|
+
const e = err as { message?: string; code?: string; type?: string }
|
|
88
|
+
return [e.message, e.code, e.type].filter(Boolean).join(" ")
|
|
89
|
+
}
|
|
90
|
+
return String(err)
|
|
91
|
+
}
|