@cartbase/storefront 0.20.0 → 0.21.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 +4 -1
- 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 +181 -180
- package/src/api/index.ts +30 -30
- package/src/api/menus.ts +77 -77
- package/src/api/metaobjects.ts +136 -136
- package/src/api/orders.ts +290 -290
- package/src/api/redirects.ts +37 -37
- package/src/api/regions.ts +200 -200
- package/src/api/search.ts +163 -163
- package/src/api/store.ts +56 -35
- package/src/api/types.ts +91 -91
- package/src/cart-drawer/context.tsx +778 -778
- package/src/cart-drawer/cross-sell-carousel.tsx +211 -211
- package/src/cart-drawer/cross-sell-sidebar.tsx +158 -158
- package/src/cart-drawer/gift-wrap.tsx +82 -82
- package/src/cart-drawer/item/index.tsx +162 -162
- package/src/cart-drawer/item/upsell.tsx +110 -110
- package/src/cart-drawer/labels.ts +123 -123
- package/src/cart-drawer/mutation-queue.ts +78 -78
- package/src/cart-drawer/notes.tsx +131 -131
- package/src/cart-drawer/payment-badges.tsx +36 -96
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/cart-drawer/summary-breakdown.tsx +197 -197
- package/src/checkout/address-error-copy.ts +117 -117
- package/src/checkout/boxnow-locker-selector.tsx +410 -410
- package/src/checkout/compare-addresses.ts +40 -40
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -63
- package/src/checkout/geocode.ts +154 -154
- package/src/checkout/gift-card-section.tsx +224 -224
- package/src/checkout/index.ts +72 -72
- package/src/checkout/labels.ts +495 -495
- package/src/checkout/payment-button.tsx +372 -372
- package/src/checkout/payment-error-copy.ts +154 -154
- package/src/checkout/promotion-error-copy.ts +91 -91
- package/src/common/country-flag.tsx +52 -52
- package/src/common/country-select.tsx +11 -11
- package/src/common/icons/cartbase-mark.ts +9 -0
- package/src/common/icons/payment-marks.ts +73 -0
- package/src/common/icons/social-marks.ts +61 -0
- package/src/common/index.ts +35 -20
- package/src/common/market-select.tsx +57 -57
- package/src/common/payment-icons.tsx +54 -0
- package/src/common/powered-by-cartbase.tsx +47 -0
- package/src/common/social-links.tsx +65 -0
- 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 +3 -0
- package/src/locales/context.ts +37 -37
- package/src/locales/en.ts +26 -26
- package/src/locales/es.ts +3 -0
- package/src/locales/index.ts +19 -19
- package/src/locales/provider.tsx +59 -59
- package/src/locales/types.ts +77 -77
- package/src/order/index.ts +62 -62
- package/src/order/labels.ts +79 -79
- package/src/order/order-totals.tsx +250 -250
- package/src/primitives/select-field.tsx +93 -93
- package/src/products/image-gallery.tsx +43 -43
- package/src/products/option-select.tsx +67 -67
- package/src/products/product-info.tsx +5 -5
- package/src/products/product-price.tsx +69 -69
- package/src/products/product-promises.tsx +61 -61
- package/src/products/product-specs.tsx +65 -65
- package/src/products/product-tabs.tsx +123 -123
- package/src/products/purchase-options.tsx +130 -130
- package/src/products/sets.ts +110 -110
- package/src/products/variant-matching.ts +71 -71
- package/src/products/variant-url.ts +74 -74
- package/src/reviews-ui/lightbox-state.ts +46 -46
- package/src/reviews-ui/review-header.tsx +166 -166
- package/src/reviews-ui/review-lightbox.tsx +271 -271
- package/src/reviews-ui/review-list.tsx +193 -193
- package/src/reviews-ui/review-widget.tsx +219 -218
- package/src/reviews-ui/widget-options.ts +55 -55
- package/src/store/category-template.tsx +136 -136
- package/src/store/index.ts +40 -40
- package/src/store/labels.ts +10 -0
- package/src/tracking/chatgpt-pixel.tsx +99 -99
- package/src/tracking/consent-init.tsx +62 -62
- package/src/tracking/events.ts +348 -348
- package/src/tracking/ga4.tsx +93 -93
- package/src/tracking/google-ads.ts +84 -84
- package/src/tracking/gtm.tsx +60 -60
- package/src/tracking/inline-script.ts +49 -49
- package/src/tracking/oaiq.ts +206 -206
- package/src/tracking/tiktok-pixel.tsx +91 -91
- package/src/tracking/track-init.tsx +56 -56
- package/src/tracking/track-order-purchase.tsx +122 -122
- package/src/tracking/ttq.ts +180 -180
- package/src/tracking/use-tracking-config.ts +54 -54
- package/theme/index.css +25 -25
|
@@ -1,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
|
+
}
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import "flag-icons/css/flag-icons.min.css"
|
|
2
|
-
|
|
3
|
-
import { cn } from "../lib/utils"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A country's flag, from its ISO-3166 alpha-2 code.
|
|
7
|
-
*
|
|
8
|
-
* WHY A SPRITE AND NOT AN EMOJI: the emoji flag (🇧🇬) costs nothing and is
|
|
9
|
-
* the obvious choice, but Windows ships no flag glyphs in Segoe UI Emoji, so
|
|
10
|
-
* a Windows shopper reads the two letters "BG" instead of a flag. `flag-icons`
|
|
11
|
-
* is SVG under a CSS class, so it renders identically on every platform. It is
|
|
12
|
-
* also the same sprite the Cartbase admin draws its flags with, so a merchant
|
|
13
|
-
* sees one country treatment across the admin and their storefront.
|
|
14
|
-
*
|
|
15
|
-
* The aspect ratio is the sprite's own 4:3, sized in `em` so a flag matches
|
|
16
|
-
* the text it sits beside at any font size.
|
|
17
|
-
*
|
|
18
|
-
* Decorative by default: a flag never carries meaning a shopper cannot get
|
|
19
|
-
* from the name next to it, so it is `aria-hidden` and screen readers read
|
|
20
|
-
* the name. Pass `title` for a tooltip when the flag stands alone.
|
|
21
|
-
*/
|
|
22
|
-
export function CountryFlag({
|
|
23
|
-
code,
|
|
24
|
-
title,
|
|
25
|
-
className,
|
|
26
|
-
}: {
|
|
27
|
-
/** ISO-3166 alpha-2, any case. Unknown or empty renders a neutral placeholder. */
|
|
28
|
-
code: string | null | undefined
|
|
29
|
-
title?: string
|
|
30
|
-
className?: string
|
|
31
|
-
}) {
|
|
32
|
-
const iso2 = code?.trim().toLowerCase()
|
|
33
|
-
|
|
34
|
-
// A placeholder rather than nothing: the box holds its space, so a field
|
|
35
|
-
// does not jump when a shopper picks their country.
|
|
36
|
-
if (!iso2 || !/^[a-z]{2}$/.test(iso2)) {
|
|
37
|
-
return (
|
|
38
|
-
<span
|
|
39
|
-
className={cn("inline-block w-[1.33em] h-[1em] rounded-[2px] bg-muted", className)}
|
|
40
|
-
aria-hidden
|
|
41
|
-
/>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<span
|
|
47
|
-
className={cn(`fi fi-${iso2}`, "inline-block w-[1.33em] h-[1em] rounded-[2px] shrink-0", className)}
|
|
48
|
-
title={title}
|
|
49
|
-
aria-hidden
|
|
50
|
-
/>
|
|
51
|
-
)
|
|
52
|
-
}
|
|
1
|
+
import "flag-icons/css/flag-icons.min.css"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A country's flag, from its ISO-3166 alpha-2 code.
|
|
7
|
+
*
|
|
8
|
+
* WHY A SPRITE AND NOT AN EMOJI: the emoji flag (🇧🇬) costs nothing and is
|
|
9
|
+
* the obvious choice, but Windows ships no flag glyphs in Segoe UI Emoji, so
|
|
10
|
+
* a Windows shopper reads the two letters "BG" instead of a flag. `flag-icons`
|
|
11
|
+
* is SVG under a CSS class, so it renders identically on every platform. It is
|
|
12
|
+
* also the same sprite the Cartbase admin draws its flags with, so a merchant
|
|
13
|
+
* sees one country treatment across the admin and their storefront.
|
|
14
|
+
*
|
|
15
|
+
* The aspect ratio is the sprite's own 4:3, sized in `em` so a flag matches
|
|
16
|
+
* the text it sits beside at any font size.
|
|
17
|
+
*
|
|
18
|
+
* Decorative by default: a flag never carries meaning a shopper cannot get
|
|
19
|
+
* from the name next to it, so it is `aria-hidden` and screen readers read
|
|
20
|
+
* the name. Pass `title` for a tooltip when the flag stands alone.
|
|
21
|
+
*/
|
|
22
|
+
export function CountryFlag({
|
|
23
|
+
code,
|
|
24
|
+
title,
|
|
25
|
+
className,
|
|
26
|
+
}: {
|
|
27
|
+
/** ISO-3166 alpha-2, any case. Unknown or empty renders a neutral placeholder. */
|
|
28
|
+
code: string | null | undefined
|
|
29
|
+
title?: string
|
|
30
|
+
className?: string
|
|
31
|
+
}) {
|
|
32
|
+
const iso2 = code?.trim().toLowerCase()
|
|
33
|
+
|
|
34
|
+
// A placeholder rather than nothing: the box holds its space, so a field
|
|
35
|
+
// does not jump when a shopper picks their country.
|
|
36
|
+
if (!iso2 || !/^[a-z]{2}$/.test(iso2)) {
|
|
37
|
+
return (
|
|
38
|
+
<span
|
|
39
|
+
className={cn("inline-block w-[1.33em] h-[1em] rounded-[2px] bg-muted", className)}
|
|
40
|
+
aria-hidden
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<span
|
|
47
|
+
className={cn(`fi fi-${iso2}`, "inline-block w-[1.33em] h-[1em] rounded-[2px] shrink-0", className)}
|
|
48
|
+
title={title}
|
|
49
|
+
aria-hidden
|
|
50
|
+
/>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `CountrySelect` is now `MarketSelect` (`common/market-select`), renamed on
|
|
3
|
-
* 2026-09-14: it lists the store's MARKETS, and a component is named for what
|
|
4
|
-
* it does (store-package card, finding 6; doctrine 14b). This path and these
|
|
5
|
-
* names are kept for ONE release so an existing import keeps compiling, and
|
|
6
|
-
* go with the release after it.
|
|
7
|
-
*
|
|
8
|
-
* @deprecated import `MarketSelect` from `@cartbase/storefront/common/market-select`.
|
|
9
|
-
*/
|
|
10
|
-
export { MarketSelect as CountrySelect } from "./market-select"
|
|
11
|
-
export type { MarketSelectProps as CountrySelectProps } from "./market-select"
|
|
1
|
+
/**
|
|
2
|
+
* `CountrySelect` is now `MarketSelect` (`common/market-select`), renamed on
|
|
3
|
+
* 2026-09-14: it lists the store's MARKETS, and a component is named for what
|
|
4
|
+
* it does (store-package card, finding 6; doctrine 14b). This path and these
|
|
5
|
+
* names are kept for ONE release so an existing import keeps compiling, and
|
|
6
|
+
* go with the release after it.
|
|
7
|
+
*
|
|
8
|
+
* @deprecated import `MarketSelect` from `@cartbase/storefront/common/market-select`.
|
|
9
|
+
*/
|
|
10
|
+
export { MarketSelect as CountrySelect } from "./market-select"
|
|
11
|
+
export type { MarketSelectProps as CountrySelectProps } from "./market-select"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by scripts/build-icons.mjs from assets/icons/cartbase. Do not
|
|
3
|
+
* edit: change the SVG file and run the script. The Cartbase wordmark, Cartbase's own.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CARTBASE_WORDMARK = {
|
|
7
|
+
viewBox: "0 0 378 98",
|
|
8
|
+
path: "M48.3649 79.4452C29.0672 79.4452 16.5705 66.8635 16.5705 48.5009C16.5705 29.6283 29.6623 17.0465 48.6199 17.0465C57.3762 17.0465 64.8572 20.362 70.638 25.2927L63.242 35.069C58.3113 31.0735 53.8057 29.2032 48.3649 29.2032C37.7384 29.2032 30.5974 36.7693 30.5974 47.9908C30.5974 59.5524 37.9085 67.2035 48.705 67.2035C54.2307 67.2035 58.6513 65.5033 63.667 61.4227L71.0631 71.2841C65.4523 76.2998 57.7162 79.4452 48.3649 79.4452ZM87.1349 79.4452C78.5488 79.4452 72.2579 74.8546 72.2579 65.5033C72.2579 56.577 79.5689 51.3913 89.1752 51.3913C93.1708 51.3913 97.1663 52.1564 100.142 53.6016V51.8164C100.142 46.3756 96.5713 44.3353 90.4504 44.3353C86.3698 44.3353 82.0342 45.2705 77.6986 46.7157L75.2333 37.4494C81.0141 35.2391 86.0298 34.2189 92.4907 34.2189C105.753 34.2189 113.489 38.8946 113.489 50.7962V78H101.332C101.332 75.9597 101.417 73.8344 101.757 70.4339H101.502C99.2066 75.4496 93.8509 79.4452 87.1349 79.4452ZM85.5197 64.3131C85.5197 67.7986 88.1551 69.2438 91.4705 69.2438C96.0612 69.2438 99.7167 66.4384 100.397 61.5927C98.1015 60.4026 94.701 59.4674 91.9806 59.4674C88.1551 59.4674 85.5197 61.2527 85.5197 64.3131ZM120.395 78V35.6641H132.382C132.382 37.6194 132.297 39.4046 131.957 42.7201H132.212C133.827 38.1295 138.163 34.2189 144.283 34.2189C146.664 34.2189 149.044 34.814 150.319 35.3241L147.004 46.6306C145.474 46.1206 144.028 45.8655 142.668 45.8655C137.398 45.8655 133.742 49.266 133.742 55.9819V78H120.395ZM172.797 79.4452C163.106 79.4452 157.92 73.9194 157.92 63.2079V46.2056H150.779V35.6641H157.92V25.5477L171.182 24.1025V35.6641H181.638V46.2056H171.182V62.7829C171.182 66.4384 172.712 68.3087 176.027 68.3087C177.473 68.3087 179.343 67.8836 180.873 67.2035L184.019 76.9799C180.533 78.6801 177.048 79.4452 172.797 79.4452ZM213.752 79.4452C207.121 79.4452 201.935 76.2998 199.47 71.7091H199.215C199.385 74.3445 199.385 76.1297 199.385 78H187.398V18.4917H200.83V27.928C200.83 33.1138 200.575 37.7044 199.385 42.8051H199.555C202.275 37.9594 207.631 34.2189 214.517 34.2189C225.314 34.2189 233.645 42.3801 233.645 56.747C233.645 70.519 225.059 79.4452 213.752 79.4452ZM210.352 68.3937C216.387 68.3937 220.383 63.803 220.383 56.9171C220.383 49.7761 216.387 45.2705 210.267 45.2705C204.401 45.2705 200.49 49.9461 200.49 56.9171C200.49 63.888 204.486 68.3937 210.352 68.3937ZM250.364 79.4452C241.778 79.4452 235.487 74.8546 235.487 65.5033C235.487 56.577 242.798 51.3913 252.405 51.3913C256.4 51.3913 260.396 52.1564 263.371 53.6016V51.8164C263.371 46.3756 259.801 44.3353 253.68 44.3353C249.599 44.3353 245.264 45.2705 240.928 46.7157L238.463 37.4494C244.243 35.2391 249.259 34.2189 255.72 34.2189C268.982 34.2189 276.718 38.8946 276.718 50.7962V78H264.561C264.561 75.9597 264.646 73.8344 264.986 70.4339H264.731C262.436 75.4496 257.08 79.4452 250.364 79.4452ZM248.749 64.3131C248.749 67.7986 251.384 69.2438 254.7 69.2438C259.29 69.2438 262.946 66.4384 263.626 61.5927C261.331 60.4026 257.93 59.4674 255.21 59.4674C251.384 59.4674 248.749 61.2527 248.749 64.3131ZM298.842 79.4452C292.381 79.4452 286.175 78.085 280.479 75.1096L284.475 65.1632C289.745 67.5435 294.251 68.7337 299.182 68.7337C302.752 68.7337 304.707 67.3735 304.707 65.1632C304.707 62.7829 302.837 61.6777 297.481 60.9976C287.365 59.7225 281.924 54.9618 281.924 47.8208C281.924 39.5747 288.98 34.2189 299.692 34.2189C304.707 34.2189 311.168 35.6641 315.419 37.7894L311.848 47.6508C308.193 45.6955 303.092 44.5053 299.522 44.5053C296.291 44.5053 294.421 45.6955 294.421 47.9908C294.421 50.1161 296.206 51.2213 300.372 51.7314C312.188 53.1766 317.289 57.3421 317.289 65.3332C317.289 73.9194 310.148 79.4452 298.842 79.4452ZM342.222 79.4452C329.3 79.4452 318.928 71.4541 318.928 56.407C318.928 41.7 329.98 34.2189 341.711 34.2189C353.528 34.2189 362.369 41.4449 362.369 54.7068C362.369 56.407 362.199 58.2773 361.859 60.0625H332.275C332.615 66.2684 337.631 69.1588 342.902 69.1588C347.067 69.1588 351.063 67.8836 354.293 65.2482L359.904 73.1543C355.568 77.3199 349.022 79.4452 342.222 79.4452ZM332.36 52.4114H349.703C349.788 46.9707 345.962 44.2503 341.371 44.2503C336.781 44.2503 332.955 46.8857 332.36 52.4114Z",
|
|
9
|
+
} as const
|