@cartbase/storefront 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -8
- package/src/cart-drawer/cross-sell-carousel.tsx +2 -3
- package/src/cart-drawer/gift-wrap.tsx +82 -83
- package/src/cart-drawer/index.ts +0 -1
- package/src/cart-drawer/item/index.tsx +4 -6
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/checkout/address-error-copy.ts +117 -119
- package/src/checkout/discount-section.tsx +218 -218
- package/src/checkout/error-copy-codes.ts +63 -0
- package/src/checkout/gift-card-section.tsx +4 -4
- package/src/checkout/index.ts +4 -5
- package/src/checkout/labels.ts +218 -0
- package/src/checkout/line-item-card.tsx +3 -3
- package/src/checkout/order-summary.tsx +11 -12
- package/src/checkout/payment-button.tsx +372 -373
- package/src/checkout/payment-error-copy.ts +154 -269
- package/src/checkout/promotion-error-copy.ts +91 -124
- package/src/checkout/shipping-method-list.tsx +2 -2
- package/src/checkout/use-checkout-orchestration.ts +10 -8
- package/src/lib/money.ts +1 -1
- package/src/lib/price.tsx +39 -0
- package/src/locales/bg.ts +468 -0
- package/src/locales/en.ts +26 -0
- package/src/locales/es.ts +467 -0
- package/src/locales/index.ts +19 -0
- package/src/locales/provider.tsx +63 -0
- package/src/locales/types.ts +49 -0
- package/src/order/index.ts +0 -1
- package/src/order/order-delivery-card.tsx +2 -2
- package/src/order/order-item.tsx +3 -3
- package/src/order/order-totals.tsx +4 -4
- package/src/products/index.ts +0 -1
- package/src/reviews-ui/index.ts +0 -1
- package/src/store/index.ts +0 -1
- package/src/tracking/attribution.ts +13 -0
- package/src/tracking/chatgpt-pixel.tsx +92 -0
- package/src/tracking/consent-banner.tsx +5 -1
- package/src/tracking/consent.ts +12 -0
- package/src/tracking/events.ts +56 -2
- package/src/tracking/index.ts +12 -0
- package/src/tracking/oaiq.ts +206 -0
- package/src/tracking/storefront-tags.tsx +2 -0
- package/src/tracking/types.ts +4 -0
- package/src/cart-drawer/labels-bg.ts +0 -72
- package/src/checkout/labels-bg.ts +0 -128
- package/src/lib/dual-price.tsx +0 -73
- package/src/order/labels-bg.ts +0 -39
- package/src/products/labels-bg.ts +0 -35
- package/src/reviews-ui/labels-bg.ts +0 -91
- package/src/store/labels-bg.ts +0 -22
|
@@ -1,124 +1,91 @@
|
|
|
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
|
-
opts?: { hasEmail?: boolean }
|
|
93
|
-
): string {
|
|
94
|
-
if (err && typeof err === "object") {
|
|
95
|
-
const code = (err as { code?: unknown }).code
|
|
96
|
-
if (typeof code === "string" && PROMOTION_ERROR_CODE_COPY[code]) {
|
|
97
|
-
return PROMOTION_ERROR_CODE_COPY[code]
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const raw = extractMessage(err)
|
|
102
|
-
|
|
103
|
-
for (const { match, copy } of PROMO_PATTERNS) {
|
|
104
|
-
if (match.test(raw)) return copy
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Unknown / opaque error. If the cart has no email yet, the
|
|
108
|
-
// campaign-budget case is by far the most common trigger — point the
|
|
109
|
-
// shopper at the fix rather than a dead end.
|
|
110
|
-
if (opts?.hasEmail === false) return NEEDS_EMAIL
|
|
111
|
-
|
|
112
|
-
return GENERIC
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function extractMessage(err: unknown): string {
|
|
116
|
-
if (!err) return ""
|
|
117
|
-
if (typeof err === "string") return err
|
|
118
|
-
if (err instanceof Error) return err.message
|
|
119
|
-
if (typeof err === "object" && err !== null) {
|
|
120
|
-
const e = err as { message?: string; code?: string; type?: string }
|
|
121
|
-
return [e.message, e.code, e.type].filter(Boolean).join(" ")
|
|
122
|
-
}
|
|
123
|
-
return String(err)
|
|
124
|
-
}
|
|
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
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { StorefrontClient } from "../api/http"
|
|
4
4
|
import type { StoreShippingOption } from "../api/checkout"
|
|
5
5
|
import type { BoxNowLocker } from "../api/integrations"
|
|
6
|
-
import {
|
|
6
|
+
import { Price } from "../lib/price"
|
|
7
7
|
import { cn } from "../lib/utils"
|
|
8
8
|
import { useCheckoutLabels } from "./context"
|
|
9
9
|
import {
|
|
@@ -273,7 +273,7 @@ export function CheckoutShippingMethodList({
|
|
|
273
273
|
isFree ? (
|
|
274
274
|
labels.shippingFree
|
|
275
275
|
) : (
|
|
276
|
-
<
|
|
276
|
+
<Price amount={price} currencyCode={currencyCode} />
|
|
277
277
|
)
|
|
278
278
|
) : isLoadingPrices ? (
|
|
279
279
|
<svg
|
|
@@ -42,7 +42,7 @@ import { isStripeLike } from "../lib/payment-constants"
|
|
|
42
42
|
import compareAddresses from "./compare-addresses"
|
|
43
43
|
import { translateAddressError } from "./address-error-copy"
|
|
44
44
|
import { translatePaymentError } from "./payment-error-copy"
|
|
45
|
-
import { useOrderConfirmedPath } from "./context"
|
|
45
|
+
import { useCheckoutLabels, useOrderConfirmedPath } from "./context"
|
|
46
46
|
import type { EcontOffice } from "./econt-office-selector"
|
|
47
47
|
import type { BoxNowLocker } from "../api/integrations"
|
|
48
48
|
|
|
@@ -267,6 +267,9 @@ export function useCheckoutOrchestration({
|
|
|
267
267
|
logError: logErrorProp,
|
|
268
268
|
debug = false,
|
|
269
269
|
}: UseCheckoutOrchestrationOptions) {
|
|
270
|
+
// The store's language, for the sentences a shopper reads when a save
|
|
271
|
+
// fails. English unless a locale pack is mounted above.
|
|
272
|
+
const checkoutLabels = useCheckoutLabels()
|
|
270
273
|
// The default sink: the platform's own checkout error log. Browser money
|
|
271
274
|
// failures (Stripe confirm errors, failed 3DS returns, place-order
|
|
272
275
|
// rejections) land in checkout_error_logs step `browser` with no merchant
|
|
@@ -578,12 +581,11 @@ export function useCheckoutOrchestration({
|
|
|
578
581
|
postal_code: formData["shipping_address.postal_code"] || null,
|
|
579
582
|
}
|
|
580
583
|
)
|
|
581
|
-
//
|
|
582
|
-
// English
|
|
583
|
-
// show a
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
setAddressError(translateAddressError(errObj ?? e))
|
|
584
|
+
// Into the store's own language. The raw error.message is technical
|
|
585
|
+
// English or a validation envelope, and neither is acceptable to
|
|
586
|
+
// show a shopper at the moment of failure. Unknown errors get the
|
|
587
|
+
// generic sentence rather than half-translated text.
|
|
588
|
+
setAddressError(translateAddressError(errObj ?? e, checkoutLabels.addressErrors))
|
|
587
589
|
} finally {
|
|
588
590
|
setAddressSaving(false)
|
|
589
591
|
addressSavingRef.current = false
|
|
@@ -1079,7 +1081,7 @@ export function useCheckoutOrchestration({
|
|
|
1079
1081
|
|
|
1080
1082
|
if (redirectStatus === "succeeded") {
|
|
1081
1083
|
placeOrder().catch((err: unknown) => {
|
|
1082
|
-
const translated = translatePaymentError(err, "card")
|
|
1084
|
+
const translated = translatePaymentError(err, "card", checkoutLabels.paymentErrors)
|
|
1083
1085
|
setPaymentError(translated)
|
|
1084
1086
|
logError?.(
|
|
1085
1087
|
"place_order_error",
|
package/src/lib/money.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Money utilities — pure, side-effect-free currency formatting helpers.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from mindpages-storefront src/lib/util/money.ts + isEmpty.ts.
|
|
5
|
-
* Used by
|
|
5
|
+
* Used by Price and any other price rendering in the library.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
function isEmpty(value: unknown): boolean {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { convertToLocale } from "./money"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Price — render an amount in its own currency, and nothing else.
|
|
7
|
+
*
|
|
8
|
+
* Replaces `DualPrice` (deleted 2026-09-13, Alexander: "clean and clear
|
|
9
|
+
* the dual pricing"). That component appended a grey BGN leg to every EUR
|
|
10
|
+
* price for Bulgaria's euro-changeover disclosure rule, with the statutory
|
|
11
|
+
* 1.95583 rate baked in. Cartbase prices in EUR (see the EUR-only law) and
|
|
12
|
+
* the second currency belongs to a store's own locale work if it ever
|
|
13
|
+
* wants one, not to every price in the library.
|
|
14
|
+
*
|
|
15
|
+
* Stateless and side-effect-free, so it renders in server and client
|
|
16
|
+
* components alike, and it is deliberately NOT a client component: the old
|
|
17
|
+
* one carried "use client" for no reason, which pulled every price
|
|
18
|
+
* rendering into the client bundle.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <Price amount={19.99} currencyCode="eur" /> // "€19.99"
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type PriceProps = {
|
|
25
|
+
/** Amount in major units of the currency (19.99 for €19.99). */
|
|
26
|
+
amount: number
|
|
27
|
+
/** ISO currency code, case-insensitive. */
|
|
28
|
+
currencyCode: string
|
|
29
|
+
/** Class for the span. */
|
|
30
|
+
className?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Price({ amount, currencyCode, className }: PriceProps): React.ReactElement {
|
|
34
|
+
return (
|
|
35
|
+
<span className={className}>
|
|
36
|
+
{convertToLocale({ amount, currency_code: currencyCode })}
|
|
37
|
+
</span>
|
|
38
|
+
)
|
|
39
|
+
}
|