@cartbase/storefront 0.17.0 → 0.18.1
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 +3 -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 +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 +367 -168
- 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 +73 -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 +1604 -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/cookie-names.ts +14 -0
- 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,133 +1,133 @@
|
|
|
1
|
-
import { getPercentageDiff } from "./get-percentage-diff"
|
|
2
|
-
import { convertToLocale } from "./money"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Product price extraction — ported from
|
|
6
|
-
* `@1click/ui/src/lib/get-product-price.ts` (v2.3.1). The
|
|
7
|
-
* `@medusajs/types` `HttpTypes.StoreProduct` parameter is replaced by the
|
|
8
|
-
* structural shapes below so this module stays decoupled from the SDK
|
|
9
|
-
* domain modules (Cartbase's `variant.calculated_price` is the b2b-v1
|
|
10
|
-
* pricing context documented in `@cartbase/storefront/api/types`
|
|
11
|
-
* `CalculatedPrice`, decimal EUR major units).
|
|
12
|
-
*
|
|
13
|
-
* Wire-shape note (code truth, `src/lib/products/pricing.ts`): Cartbase serves
|
|
14
|
-
* `calculated_price.price_list_type` FLAT on the calculated_price object.
|
|
15
|
-
* The Medusa wire shape ALSO nests a `calculated_price.calculated_price`
|
|
16
|
-
* detail object carrying `price_list_type` — the original library read the
|
|
17
|
-
* nested path. `getPricesForVariant` reads the flat key first and falls
|
|
18
|
-
* back to the nested path so both shapes produce the same result.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** Structural `variant.calculated_price` shape (see api/types CalculatedPrice). */
|
|
22
|
-
export type CalculatedPriceLike = {
|
|
23
|
-
calculated_amount: number
|
|
24
|
-
original_amount: number
|
|
25
|
-
currency_code: string
|
|
26
|
-
price_list_type?: string | null
|
|
27
|
-
/** Medusa-wire nested detail object (fallback path). */
|
|
28
|
-
calculated_price?: { price_list_type?: string | null } | null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Structural variant shape — any richer product-variant DTO satisfies it. */
|
|
32
|
-
export type VariantWithPrice = {
|
|
33
|
-
id?: string
|
|
34
|
-
sku?: string | null
|
|
35
|
-
calculated_price?: CalculatedPriceLike | null
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Structural product shape — any richer product DTO satisfies it. */
|
|
39
|
-
export type ProductWithVariants = {
|
|
40
|
-
id?: string
|
|
41
|
-
variants?: VariantWithPrice[] | null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type VariantPrice = {
|
|
45
|
-
calculated_price_number: number
|
|
46
|
-
calculated_price: string
|
|
47
|
-
original_price_number: number
|
|
48
|
-
original_price: string
|
|
49
|
-
currency_code: string
|
|
50
|
-
price_type: string | null | undefined
|
|
51
|
-
percentage_diff: string
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const getPricesForVariant = (
|
|
55
|
-
variant: VariantWithPrice | null | undefined
|
|
56
|
-
): VariantPrice | null => {
|
|
57
|
-
if (!variant?.calculated_price?.calculated_amount) {
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const cp = variant.calculated_price
|
|
62
|
-
return {
|
|
63
|
-
calculated_price_number: cp.calculated_amount,
|
|
64
|
-
calculated_price: convertToLocale({
|
|
65
|
-
amount: cp.calculated_amount,
|
|
66
|
-
currency_code: cp.currency_code,
|
|
67
|
-
}),
|
|
68
|
-
original_price_number: cp.original_amount,
|
|
69
|
-
original_price: convertToLocale({
|
|
70
|
-
amount: cp.original_amount,
|
|
71
|
-
currency_code: cp.currency_code,
|
|
72
|
-
}),
|
|
73
|
-
currency_code: cp.currency_code,
|
|
74
|
-
// Flat key (Cartbase wire shape) first — an explicit null stays null;
|
|
75
|
-
// the nested Medusa detail is the fallback only when the flat key is
|
|
76
|
-
// absent entirely (the original @1click read path).
|
|
77
|
-
price_type:
|
|
78
|
-
cp.price_list_type !== undefined
|
|
79
|
-
? cp.price_list_type
|
|
80
|
-
: cp.calculated_price?.price_list_type,
|
|
81
|
-
percentage_diff: getPercentageDiff(cp.original_amount, cp.calculated_amount),
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function getProductPrice({
|
|
86
|
-
product,
|
|
87
|
-
variantId,
|
|
88
|
-
}: {
|
|
89
|
-
product: ProductWithVariants
|
|
90
|
-
variantId?: string
|
|
91
|
-
}) {
|
|
92
|
-
if (!product || !product.id) {
|
|
93
|
-
throw new Error("No product provided")
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const cheapestPrice = (): VariantPrice | null => {
|
|
97
|
-
if (!product || !product.variants?.length) {
|
|
98
|
-
return null
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const cheapestVariant = product.variants
|
|
102
|
-
.filter((v) => !!v.calculated_price)
|
|
103
|
-
.sort(
|
|
104
|
-
(a, b) =>
|
|
105
|
-
(a.calculated_price?.calculated_amount ?? 0) -
|
|
106
|
-
(b.calculated_price?.calculated_amount ?? 0)
|
|
107
|
-
)[0]
|
|
108
|
-
|
|
109
|
-
return getPricesForVariant(cheapestVariant)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const variantPrice = (): VariantPrice | null => {
|
|
113
|
-
if (!product || !variantId) {
|
|
114
|
-
return null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const variant = product.variants?.find(
|
|
118
|
-
(v) => v.id === variantId || v.sku === variantId
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
if (!variant) {
|
|
122
|
-
return null
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return getPricesForVariant(variant)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
product,
|
|
130
|
-
cheapestPrice: cheapestPrice(),
|
|
131
|
-
variantPrice: variantPrice(),
|
|
132
|
-
}
|
|
133
|
-
}
|
|
1
|
+
import { getPercentageDiff } from "./get-percentage-diff"
|
|
2
|
+
import { convertToLocale } from "./money"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Product price extraction — ported from
|
|
6
|
+
* `@1click/ui/src/lib/get-product-price.ts` (v2.3.1). The
|
|
7
|
+
* `@medusajs/types` `HttpTypes.StoreProduct` parameter is replaced by the
|
|
8
|
+
* structural shapes below so this module stays decoupled from the SDK
|
|
9
|
+
* domain modules (Cartbase's `variant.calculated_price` is the b2b-v1
|
|
10
|
+
* pricing context documented in `@cartbase/storefront/api/types`
|
|
11
|
+
* `CalculatedPrice`, decimal EUR major units).
|
|
12
|
+
*
|
|
13
|
+
* Wire-shape note (code truth, `src/lib/products/pricing.ts`): Cartbase serves
|
|
14
|
+
* `calculated_price.price_list_type` FLAT on the calculated_price object.
|
|
15
|
+
* The Medusa wire shape ALSO nests a `calculated_price.calculated_price`
|
|
16
|
+
* detail object carrying `price_list_type` — the original library read the
|
|
17
|
+
* nested path. `getPricesForVariant` reads the flat key first and falls
|
|
18
|
+
* back to the nested path so both shapes produce the same result.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Structural `variant.calculated_price` shape (see api/types CalculatedPrice). */
|
|
22
|
+
export type CalculatedPriceLike = {
|
|
23
|
+
calculated_amount: number
|
|
24
|
+
original_amount: number
|
|
25
|
+
currency_code: string
|
|
26
|
+
price_list_type?: string | null
|
|
27
|
+
/** Medusa-wire nested detail object (fallback path). */
|
|
28
|
+
calculated_price?: { price_list_type?: string | null } | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Structural variant shape — any richer product-variant DTO satisfies it. */
|
|
32
|
+
export type VariantWithPrice = {
|
|
33
|
+
id?: string
|
|
34
|
+
sku?: string | null
|
|
35
|
+
calculated_price?: CalculatedPriceLike | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Structural product shape — any richer product DTO satisfies it. */
|
|
39
|
+
export type ProductWithVariants = {
|
|
40
|
+
id?: string
|
|
41
|
+
variants?: VariantWithPrice[] | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type VariantPrice = {
|
|
45
|
+
calculated_price_number: number
|
|
46
|
+
calculated_price: string
|
|
47
|
+
original_price_number: number
|
|
48
|
+
original_price: string
|
|
49
|
+
currency_code: string
|
|
50
|
+
price_type: string | null | undefined
|
|
51
|
+
percentage_diff: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const getPricesForVariant = (
|
|
55
|
+
variant: VariantWithPrice | null | undefined
|
|
56
|
+
): VariantPrice | null => {
|
|
57
|
+
if (!variant?.calculated_price?.calculated_amount) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const cp = variant.calculated_price
|
|
62
|
+
return {
|
|
63
|
+
calculated_price_number: cp.calculated_amount,
|
|
64
|
+
calculated_price: convertToLocale({
|
|
65
|
+
amount: cp.calculated_amount,
|
|
66
|
+
currency_code: cp.currency_code,
|
|
67
|
+
}),
|
|
68
|
+
original_price_number: cp.original_amount,
|
|
69
|
+
original_price: convertToLocale({
|
|
70
|
+
amount: cp.original_amount,
|
|
71
|
+
currency_code: cp.currency_code,
|
|
72
|
+
}),
|
|
73
|
+
currency_code: cp.currency_code,
|
|
74
|
+
// Flat key (Cartbase wire shape) first — an explicit null stays null;
|
|
75
|
+
// the nested Medusa detail is the fallback only when the flat key is
|
|
76
|
+
// absent entirely (the original @1click read path).
|
|
77
|
+
price_type:
|
|
78
|
+
cp.price_list_type !== undefined
|
|
79
|
+
? cp.price_list_type
|
|
80
|
+
: cp.calculated_price?.price_list_type,
|
|
81
|
+
percentage_diff: getPercentageDiff(cp.original_amount, cp.calculated_amount),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getProductPrice({
|
|
86
|
+
product,
|
|
87
|
+
variantId,
|
|
88
|
+
}: {
|
|
89
|
+
product: ProductWithVariants
|
|
90
|
+
variantId?: string
|
|
91
|
+
}) {
|
|
92
|
+
if (!product || !product.id) {
|
|
93
|
+
throw new Error("No product provided")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cheapestPrice = (): VariantPrice | null => {
|
|
97
|
+
if (!product || !product.variants?.length) {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const cheapestVariant = product.variants
|
|
102
|
+
.filter((v) => !!v.calculated_price)
|
|
103
|
+
.sort(
|
|
104
|
+
(a, b) =>
|
|
105
|
+
(a.calculated_price?.calculated_amount ?? 0) -
|
|
106
|
+
(b.calculated_price?.calculated_amount ?? 0)
|
|
107
|
+
)[0]
|
|
108
|
+
|
|
109
|
+
return getPricesForVariant(cheapestVariant)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const variantPrice = (): VariantPrice | null => {
|
|
113
|
+
if (!product || !variantId) {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const variant = product.variants?.find(
|
|
118
|
+
(v) => v.id === variantId || v.sku === variantId
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (!variant) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return getPricesForVariant(variant)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
product,
|
|
130
|
+
cheapestPrice: cheapestPrice(),
|
|
131
|
+
variantPrice: variantPrice(),
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/lib/media-image.tsx
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import NextImage, { type ImageProps } from "next/image"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The store's picture element (store-speed-law card, step 3, 2026-09-15).
|
|
5
|
-
*
|
|
6
|
-
* Product pictures live on the Cartbase media CDN, and Next resizes them on
|
|
7
|
-
* the store's own project, per device and in the modern formats, which the
|
|
8
|
-
* scaffold allows through `images.remotePatterns` (next.config.ts). The
|
|
9
|
-
* scaffold used to ship `images.unoptimized: true` instead, so no picture
|
|
10
|
-
* was ever resized: a phone downloaded the merchant's full upload.
|
|
11
|
-
*
|
|
12
|
-
* A picture from any other host (a catalogue not yet rehosted, a demo
|
|
13
|
-
* placeholder, a merchant's own asset host) renders as it is instead of
|
|
14
|
-
* failing the page: Next refuses to resize a host the config does not
|
|
15
|
-
* list, so this element switches resizing off for those. A merchant who
|
|
16
|
-
* adds their host to `remotePatterns` gets resizing there too by passing
|
|
17
|
-
* `unoptimized={false}` where they render it, or by using `next/image`
|
|
18
|
-
* directly.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** The public base of the media CDN (REGISTRY.md, CDN domain). */
|
|
22
|
-
export const MEDIA_HOST = "cartbase.cloud"
|
|
23
|
-
|
|
24
|
-
/** Is this picture served by the media CDN (or by the app itself)? */
|
|
25
|
-
export function isResizableSrc(src: ImageProps["src"]): boolean {
|
|
26
|
-
if (typeof src !== "string") return true
|
|
27
|
-
if (src.startsWith("/")) return true
|
|
28
|
-
try {
|
|
29
|
-
const host = new URL(src).hostname
|
|
30
|
-
return host === MEDIA_HOST || host.endsWith(`.${MEDIA_HOST}`)
|
|
31
|
-
} catch {
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function StoreImage(props: ImageProps) {
|
|
37
|
-
const unoptimized = props.unoptimized ?? !isResizableSrc(props.src)
|
|
38
|
-
return <NextImage {...props} unoptimized={unoptimized} />
|
|
39
|
-
}
|
|
1
|
+
import NextImage, { type ImageProps } from "next/image"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The store's picture element (store-speed-law card, step 3, 2026-09-15).
|
|
5
|
+
*
|
|
6
|
+
* Product pictures live on the Cartbase media CDN, and Next resizes them on
|
|
7
|
+
* the store's own project, per device and in the modern formats, which the
|
|
8
|
+
* scaffold allows through `images.remotePatterns` (next.config.ts). The
|
|
9
|
+
* scaffold used to ship `images.unoptimized: true` instead, so no picture
|
|
10
|
+
* was ever resized: a phone downloaded the merchant's full upload.
|
|
11
|
+
*
|
|
12
|
+
* A picture from any other host (a catalogue not yet rehosted, a demo
|
|
13
|
+
* placeholder, a merchant's own asset host) renders as it is instead of
|
|
14
|
+
* failing the page: Next refuses to resize a host the config does not
|
|
15
|
+
* list, so this element switches resizing off for those. A merchant who
|
|
16
|
+
* adds their host to `remotePatterns` gets resizing there too by passing
|
|
17
|
+
* `unoptimized={false}` where they render it, or by using `next/image`
|
|
18
|
+
* directly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The public base of the media CDN (REGISTRY.md, CDN domain). */
|
|
22
|
+
export const MEDIA_HOST = "cartbase.cloud"
|
|
23
|
+
|
|
24
|
+
/** Is this picture served by the media CDN (or by the app itself)? */
|
|
25
|
+
export function isResizableSrc(src: ImageProps["src"]): boolean {
|
|
26
|
+
if (typeof src !== "string") return true
|
|
27
|
+
if (src.startsWith("/")) return true
|
|
28
|
+
try {
|
|
29
|
+
const host = new URL(src).hostname
|
|
30
|
+
return host === MEDIA_HOST || host.endsWith(`.${MEDIA_HOST}`)
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function StoreImage(props: ImageProps) {
|
|
37
|
+
const unoptimized = props.unoptimized ?? !isResizableSrc(props.src)
|
|
38
|
+
return <NextImage {...props} unoptimized={unoptimized} />
|
|
39
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Payment tender helpers — identifier sniffing for store payment sessions.
|
|
3
|
-
*
|
|
4
|
-
* The Cartbase wire has exactly TWO tender shapes since the pp_* kill
|
|
5
|
-
* (20260811250000, no-payment-method-by-default card):
|
|
6
|
-
*
|
|
7
|
-
* - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
|
|
8
|
-
* today; PayPal etc. as integrations land);
|
|
9
|
-
* - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
|
|
10
|
-
* `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
|
|
11
|
-
* collection-on-delivery switch). Its session carries `provider_id`
|
|
12
|
-
* NULL; the merchant's NAME is the display everywhere.
|
|
13
|
-
*
|
|
14
|
-
* The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
|
|
15
|
-
* the wire and from this module. The Medusa-era Stripe prefixes
|
|
16
|
-
* (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
|
|
17
|
-
* ported later keep working against either wire.
|
|
18
|
-
*
|
|
19
|
-
* Intentionally ships NO icons — stores render their own icons via their
|
|
20
|
-
* own icon set. The library stays free of icon-kit imports at this layer.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* True if the provider id is a Stripe-backed card payment provider.
|
|
25
|
-
* Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
|
|
26
|
-
* Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
|
|
27
|
-
*/
|
|
28
|
-
export const isStripeLike = (providerId?: string | null): boolean => {
|
|
29
|
-
return Boolean(
|
|
30
|
-
providerId &&
|
|
31
|
-
(providerId === "pp_stripe" ||
|
|
32
|
-
providerId === "stripe" ||
|
|
33
|
-
providerId.startsWith("pp_stripe_") ||
|
|
34
|
-
providerId.startsWith("pp_medusa-"))
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** True if the provider id is PayPal. */
|
|
39
|
-
export const isPaypal = (providerId?: string | null): boolean => {
|
|
40
|
-
return Boolean(providerId?.startsWith("pp_paypal"))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const paymentInfoMap: Record<
|
|
44
|
-
string,
|
|
45
|
-
{ title: string; icon: React.JSX.Element | null }
|
|
46
|
-
> = {
|
|
47
|
-
pp_stripe: { title: "Credit / debit card", icon: null },
|
|
48
|
-
pp_stripe_stripe: { title: "Credit / debit card", icon: null },
|
|
49
|
-
"pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
|
|
50
|
-
"pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
|
|
51
|
-
"pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
|
|
52
|
-
pp_paypal_paypal: { title: "PayPal", icon: null },
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Payment tender helpers — identifier sniffing for store payment sessions.
|
|
3
|
+
*
|
|
4
|
+
* The Cartbase wire has exactly TWO tender shapes since the pp_* kill
|
|
5
|
+
* (20260811250000, no-payment-method-by-default card):
|
|
6
|
+
*
|
|
7
|
+
* - a PROCESSOR the merchant connected — `provider_id` (`pp_stripe`
|
|
8
|
+
* today; PayPal etc. as integrations land);
|
|
9
|
+
* - a merchant-operated PAYMENT METHOD — `payment_method_id` + `name` +
|
|
10
|
+
* `kind` (`manual` for Bank transfer / Pay in store, `cod` for the
|
|
11
|
+
* collection-on-delivery switch). Its session carries `provider_id`
|
|
12
|
+
* NULL; the merchant's NAME is the display everywhere.
|
|
13
|
+
*
|
|
14
|
+
* The dead ids (`pp_cod`, `pp_manual`, `pp_system_default`) are gone from
|
|
15
|
+
* the wire and from this module. The Medusa-era Stripe prefixes
|
|
16
|
+
* (`pp_stripe_*`, `pp_medusa-*`) are kept as fallbacks so components
|
|
17
|
+
* ported later keep working against either wire.
|
|
18
|
+
*
|
|
19
|
+
* Intentionally ships NO icons — stores render their own icons via their
|
|
20
|
+
* own icon set. The library stays free of icon-kit imports at this layer.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* True if the provider id is a Stripe-backed card payment provider.
|
|
25
|
+
* Cartbase's canonical ids first (`pp_stripe` / `stripe`), then the
|
|
26
|
+
* Medusa-era prefixes (`pp_stripe_*`, `pp_medusa-*`).
|
|
27
|
+
*/
|
|
28
|
+
export const isStripeLike = (providerId?: string | null): boolean => {
|
|
29
|
+
return Boolean(
|
|
30
|
+
providerId &&
|
|
31
|
+
(providerId === "pp_stripe" ||
|
|
32
|
+
providerId === "stripe" ||
|
|
33
|
+
providerId.startsWith("pp_stripe_") ||
|
|
34
|
+
providerId.startsWith("pp_medusa-"))
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** True if the provider id is PayPal. */
|
|
39
|
+
export const isPaypal = (providerId?: string | null): boolean => {
|
|
40
|
+
return Boolean(providerId?.startsWith("pp_paypal"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const paymentInfoMap: Record<
|
|
44
|
+
string,
|
|
45
|
+
{ title: string; icon: React.JSX.Element | null }
|
|
46
|
+
> = {
|
|
47
|
+
pp_stripe: { title: "Credit / debit card", icon: null },
|
|
48
|
+
pp_stripe_stripe: { title: "Credit / debit card", icon: null },
|
|
49
|
+
"pp_medusa-payments_default": { title: "Credit / debit card", icon: null },
|
|
50
|
+
"pp_stripe-ideal_stripe": { title: "iDeal", icon: null },
|
|
51
|
+
"pp_stripe-bancontact_stripe": { title: "Bancontact", icon: null },
|
|
52
|
+
pp_paypal_paypal: { title: "PayPal", icon: null },
|
|
53
|
+
}
|
package/src/lib/platform.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
|
|
3
|
-
* a constant, not an input. Every Cartbase store's API lives here; a
|
|
4
|
-
* storefront overrides it only to point at a local or staging platform
|
|
5
|
-
* (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
|
|
6
|
-
* publishable key does.
|
|
7
|
-
*/
|
|
8
|
-
export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
|
|
9
|
-
|
|
10
|
-
/** The origin to call: the override when set, else the platform. */
|
|
11
|
-
export function cartbaseApiOrigin(override?: string | null): string {
|
|
12
|
-
return (override && override.trim()) || CARTBASE_API_ORIGIN
|
|
13
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The platform's origin (one key, store-birth-and-doors item 6, 2026-09-07):
|
|
3
|
+
* a constant, not an input. Every Cartbase store's API lives here; a
|
|
4
|
+
* storefront overrides it only to point at a local or staging platform
|
|
5
|
+
* (`NEXT_PUBLIC_CARTBASE_URL`), never to find its own store, which the
|
|
6
|
+
* publishable key does.
|
|
7
|
+
*/
|
|
8
|
+
export const CARTBASE_API_ORIGIN = "https://admin.cartbase.ai"
|
|
9
|
+
|
|
10
|
+
/** The origin to call: the override when set, else the platform. */
|
|
11
|
+
export function cartbaseApiOrigin(override?: string | null): string {
|
|
12
|
+
return (override && override.trim()) || CARTBASE_API_ORIGIN
|
|
13
|
+
}
|
package/src/lib/price.tsx
CHANGED
|
@@ -1,39 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { StoreApiError } from "../api/types"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* storeApiError — the successor of `@1click/ui`'s `medusaError`.
|
|
5
|
-
*
|
|
6
|
-
* Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
|
|
7
|
-
* clean, user-presentable `Error` whose message starts with a capital and
|
|
8
|
-
* ends with a period — the same normalization contract the Medusa-era
|
|
9
|
-
* helper enforced, adapted to the Cartbase error contract (`StoreApiError`
|
|
10
|
-
* with `status` + `code` + raw `body`, see `../api/types`).
|
|
11
|
-
*
|
|
12
|
-
* Always throws — never returns. Use in `.catch()` chains:
|
|
13
|
-
*
|
|
14
|
-
* addLineItem(client, cartId, input).catch(storeApiError)
|
|
15
|
-
*
|
|
16
|
-
* Callers that need the PROGRAMMATIC surface (branching on `status` /
|
|
17
|
-
* `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
|
|
18
|
-
* `StoreApiError` directly instead — this adapter is the display-boundary
|
|
19
|
-
* normalizer, deliberately lossy about everything but the message.
|
|
20
|
-
*/
|
|
21
|
-
export default function storeApiError(error: unknown): never {
|
|
22
|
-
if (error instanceof StoreApiError) {
|
|
23
|
-
const message = error.message || `Store API error (${error.status})`
|
|
24
|
-
throw new Error(sentence(message))
|
|
25
|
-
}
|
|
26
|
-
if (error instanceof Error) {
|
|
27
|
-
throw new Error(sentence(error.message || "Unknown error"))
|
|
28
|
-
}
|
|
29
|
-
throw new Error("Error setting up the request: " + String(error))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Capitalize + terminal period — the medusaError display normalization. */
|
|
33
|
-
function sentence(message: string): string {
|
|
34
|
-
const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
|
|
35
|
-
return capitalized.endsWith(".") ? capitalized : capitalized + "."
|
|
36
|
-
}
|
|
1
|
+
import { StoreApiError } from "../api/types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* storeApiError — the successor of `@1click/ui`'s `medusaError`.
|
|
5
|
+
*
|
|
6
|
+
* Normalizes any error thrown by the `@cartbase/storefront` SDK layer into a
|
|
7
|
+
* clean, user-presentable `Error` whose message starts with a capital and
|
|
8
|
+
* ends with a period — the same normalization contract the Medusa-era
|
|
9
|
+
* helper enforced, adapted to the Cartbase error contract (`StoreApiError`
|
|
10
|
+
* with `status` + `code` + raw `body`, see `../api/types`).
|
|
11
|
+
*
|
|
12
|
+
* Always throws — never returns. Use in `.catch()` chains:
|
|
13
|
+
*
|
|
14
|
+
* addLineItem(client, cartId, input).catch(storeApiError)
|
|
15
|
+
*
|
|
16
|
+
* Callers that need the PROGRAMMATIC surface (branching on `status` /
|
|
17
|
+
* `code`, e.g. `checkout_method_hidden`, `invalid_gift_card`) should catch
|
|
18
|
+
* `StoreApiError` directly instead — this adapter is the display-boundary
|
|
19
|
+
* normalizer, deliberately lossy about everything but the message.
|
|
20
|
+
*/
|
|
21
|
+
export default function storeApiError(error: unknown): never {
|
|
22
|
+
if (error instanceof StoreApiError) {
|
|
23
|
+
const message = error.message || `Store API error (${error.status})`
|
|
24
|
+
throw new Error(sentence(message))
|
|
25
|
+
}
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
throw new Error(sentence(error.message || "Unknown error"))
|
|
28
|
+
}
|
|
29
|
+
throw new Error("Error setting up the request: " + String(error))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Capitalize + terminal period — the medusaError display normalization. */
|
|
33
|
+
function sentence(message: string): string {
|
|
34
|
+
const capitalized = message.charAt(0).toUpperCase() + message.slice(1)
|
|
35
|
+
return capitalized.endsWith(".") ? capitalized : capitalized + "."
|
|
36
|
+
}
|