@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
package/src/api/collections.ts
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/collections — collections + membership listings.
|
|
3
|
-
*
|
|
4
|
-
* Collections are the merchant's curated product groupings
|
|
5
|
-
* (collections-upgrade card): manual or smart (`conditions`/`match`), with a
|
|
6
|
-
* per-collection `default_sort` the membership listing honors and an
|
|
7
|
-
* optional per-request `order` override. Docs:
|
|
8
|
-
* docs/storefront/collections.md.
|
|
9
|
-
*/
|
|
10
|
-
import type { StorefrontClient } from "./http"
|
|
11
|
-
import type { IsoDateString, ListEnvelope, PaginationQuery, PricingContextQuery } from "./types"
|
|
12
|
-
import type { StoreProduct } from "./products"
|
|
13
|
-
|
|
14
|
-
/** Sort modes accepted by `default_sort` and the `order` override. */
|
|
15
|
-
export type CollectionSortMode =
|
|
16
|
-
| "manual"
|
|
17
|
-
| "title_asc"
|
|
18
|
-
| "title_desc"
|
|
19
|
-
| "price_asc"
|
|
20
|
-
| "price_desc"
|
|
21
|
-
| "newest"
|
|
22
|
-
| "oldest"
|
|
23
|
-
| "best_selling"
|
|
24
|
-
|
|
25
|
-
export interface StoreCollection {
|
|
26
|
-
id: string
|
|
27
|
-
title: string
|
|
28
|
-
handle: string
|
|
29
|
-
/** "manual" | "smart" (smart membership is computed from `conditions`). */
|
|
30
|
-
type: string
|
|
31
|
-
description: string | null
|
|
32
|
-
image_url: string | null
|
|
33
|
-
/** The sort the membership listing uses when no `order` override is sent. */
|
|
34
|
-
default_sort: string
|
|
35
|
-
/** Smart-collection condition rows (admin-authored; opaque to render). */
|
|
36
|
-
conditions: unknown
|
|
37
|
-
/** Smart matching: "all" | "any". */
|
|
38
|
-
match: string
|
|
39
|
-
/** SEO override; null = fall back to `title`. */
|
|
40
|
-
seo_title: string | null
|
|
41
|
-
/** SEO override; null = fall back to `description`. */
|
|
42
|
-
seo_description: string | null
|
|
43
|
-
metadata: Record<string, unknown> | null
|
|
44
|
-
created_at: IsoDateString
|
|
45
|
-
updated_at: IsoDateString
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface ListCollectionsQuery extends PaginationQuery {
|
|
49
|
-
/** Case-insensitive substring match on title. */
|
|
50
|
-
q?: string
|
|
51
|
-
handle?: string
|
|
52
|
-
/**
|
|
53
|
-
* Channel scope: a collection with channel links is visible ONLY on those
|
|
54
|
-
* channels (excluded from other channels' lists); a collection with no
|
|
55
|
-
* links is visible everywhere.
|
|
56
|
-
*/
|
|
57
|
-
sales_channel_id?: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface CollectionListResponse extends ListEnvelope {
|
|
61
|
-
collections: StoreCollection[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface CollectionResponse {
|
|
65
|
-
collection: StoreCollection
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface ListCollectionProductsQuery extends PricingContextQuery, PaginationQuery {
|
|
69
|
-
/** Sort override — any CollectionSortMode; unknown values are ignored. */
|
|
70
|
-
order?: CollectionSortMode | string
|
|
71
|
-
/** Channel scope: a collection scoped to OTHER channels 404s here. */
|
|
72
|
-
sales_channel_id?: string
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface CollectionProductsResponse extends ListEnvelope {
|
|
76
|
-
products: StoreProduct[]
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* List collections, ordered by title.
|
|
81
|
-
*
|
|
82
|
-
* Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
|
|
83
|
-
* 400 `validation_failed`. Settings: collection channel links
|
|
84
|
-
* (`sales_channel_id` scope), smart-collection conditions.
|
|
85
|
-
*/
|
|
86
|
-
export async function listCollections(
|
|
87
|
-
client: StorefrontClient,
|
|
88
|
-
query?: ListCollectionsQuery
|
|
89
|
-
): Promise<CollectionListResponse> {
|
|
90
|
-
return client.get("/api/store/collections", { query: { ...query } })
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Retrieve one collection by id. (Look up by handle via
|
|
95
|
-
* `listCollections({ handle })`.)
|
|
96
|
-
*
|
|
97
|
-
* Auth: anon (`x-client-id`). Errors: 404 `not_found`. NOTE: the single read
|
|
98
|
-
* takes no `sales_channel_id` — channel scope applies to the list and to the
|
|
99
|
-
* membership listing, not here.
|
|
100
|
-
*/
|
|
101
|
-
export async function retrieveCollection(
|
|
102
|
-
client: StorefrontClient,
|
|
103
|
-
collectionId: string
|
|
104
|
-
): Promise<CollectionResponse> {
|
|
105
|
-
return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}`)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* List a collection's products — the MEMBERSHIP join (multi-collection
|
|
110
|
-
* products appear in every collection they belong to), published products
|
|
111
|
-
* only, ordered by the collection's `default_sort` unless `order` overrides
|
|
112
|
-
* (manual position, title, EUR price asc/desc, newest/oldest, best_selling
|
|
113
|
-
* 90-day aggregate). Products carry the full `StoreProduct` shape incl.
|
|
114
|
-
* `calculated_price` when a pricing context is given.
|
|
115
|
-
*
|
|
116
|
-
* Auth: anon (`x-client-id`); optional Bearer JWT for group-aware pricing.
|
|
117
|
-
* Errors: 404 `not_found` (unknown collection, or scoped away from
|
|
118
|
-
* `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
|
|
119
|
-
* `limit` 1–100 (default 50). Settings: collection default_sort + manual
|
|
120
|
-
* order, channel links, price lists.
|
|
121
|
-
*/
|
|
122
|
-
export async function listCollectionProducts(
|
|
123
|
-
client: StorefrontClient,
|
|
124
|
-
collectionId: string,
|
|
125
|
-
query?: ListCollectionProductsQuery
|
|
126
|
-
): Promise<CollectionProductsResponse> {
|
|
127
|
-
return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}/products`, {
|
|
128
|
-
query: { ...query },
|
|
129
|
-
})
|
|
130
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/collections — collections + membership listings.
|
|
3
|
+
*
|
|
4
|
+
* Collections are the merchant's curated product groupings
|
|
5
|
+
* (collections-upgrade card): manual or smart (`conditions`/`match`), with a
|
|
6
|
+
* per-collection `default_sort` the membership listing honors and an
|
|
7
|
+
* optional per-request `order` override. Docs:
|
|
8
|
+
* docs/storefront/collections.md.
|
|
9
|
+
*/
|
|
10
|
+
import type { StorefrontClient } from "./http"
|
|
11
|
+
import type { IsoDateString, ListEnvelope, PaginationQuery, PricingContextQuery } from "./types"
|
|
12
|
+
import type { StoreProduct } from "./products"
|
|
13
|
+
|
|
14
|
+
/** Sort modes accepted by `default_sort` and the `order` override. */
|
|
15
|
+
export type CollectionSortMode =
|
|
16
|
+
| "manual"
|
|
17
|
+
| "title_asc"
|
|
18
|
+
| "title_desc"
|
|
19
|
+
| "price_asc"
|
|
20
|
+
| "price_desc"
|
|
21
|
+
| "newest"
|
|
22
|
+
| "oldest"
|
|
23
|
+
| "best_selling"
|
|
24
|
+
|
|
25
|
+
export interface StoreCollection {
|
|
26
|
+
id: string
|
|
27
|
+
title: string
|
|
28
|
+
handle: string
|
|
29
|
+
/** "manual" | "smart" (smart membership is computed from `conditions`). */
|
|
30
|
+
type: string
|
|
31
|
+
description: string | null
|
|
32
|
+
image_url: string | null
|
|
33
|
+
/** The sort the membership listing uses when no `order` override is sent. */
|
|
34
|
+
default_sort: string
|
|
35
|
+
/** Smart-collection condition rows (admin-authored; opaque to render). */
|
|
36
|
+
conditions: unknown
|
|
37
|
+
/** Smart matching: "all" | "any". */
|
|
38
|
+
match: string
|
|
39
|
+
/** SEO override; null = fall back to `title`. */
|
|
40
|
+
seo_title: string | null
|
|
41
|
+
/** SEO override; null = fall back to `description`. */
|
|
42
|
+
seo_description: string | null
|
|
43
|
+
metadata: Record<string, unknown> | null
|
|
44
|
+
created_at: IsoDateString
|
|
45
|
+
updated_at: IsoDateString
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ListCollectionsQuery extends PaginationQuery {
|
|
49
|
+
/** Case-insensitive substring match on title. */
|
|
50
|
+
q?: string
|
|
51
|
+
handle?: string
|
|
52
|
+
/**
|
|
53
|
+
* Channel scope: a collection with channel links is visible ONLY on those
|
|
54
|
+
* channels (excluded from other channels' lists); a collection with no
|
|
55
|
+
* links is visible everywhere.
|
|
56
|
+
*/
|
|
57
|
+
sales_channel_id?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CollectionListResponse extends ListEnvelope {
|
|
61
|
+
collections: StoreCollection[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CollectionResponse {
|
|
65
|
+
collection: StoreCollection
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ListCollectionProductsQuery extends PricingContextQuery, PaginationQuery {
|
|
69
|
+
/** Sort override — any CollectionSortMode; unknown values are ignored. */
|
|
70
|
+
order?: CollectionSortMode | string
|
|
71
|
+
/** Channel scope: a collection scoped to OTHER channels 404s here. */
|
|
72
|
+
sales_channel_id?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface CollectionProductsResponse extends ListEnvelope {
|
|
76
|
+
products: StoreProduct[]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List collections, ordered by title.
|
|
81
|
+
*
|
|
82
|
+
* Auth: anon (`x-client-id`). Errors: 400 `missing_client_id`,
|
|
83
|
+
* 400 `validation_failed`. Settings: collection channel links
|
|
84
|
+
* (`sales_channel_id` scope), smart-collection conditions.
|
|
85
|
+
*/
|
|
86
|
+
export async function listCollections(
|
|
87
|
+
client: StorefrontClient,
|
|
88
|
+
query?: ListCollectionsQuery
|
|
89
|
+
): Promise<CollectionListResponse> {
|
|
90
|
+
return client.get("/api/store/collections", { query: { ...query } })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Retrieve one collection by id. (Look up by handle via
|
|
95
|
+
* `listCollections({ handle })`.)
|
|
96
|
+
*
|
|
97
|
+
* Auth: anon (`x-client-id`). Errors: 404 `not_found`. NOTE: the single read
|
|
98
|
+
* takes no `sales_channel_id` — channel scope applies to the list and to the
|
|
99
|
+
* membership listing, not here.
|
|
100
|
+
*/
|
|
101
|
+
export async function retrieveCollection(
|
|
102
|
+
client: StorefrontClient,
|
|
103
|
+
collectionId: string
|
|
104
|
+
): Promise<CollectionResponse> {
|
|
105
|
+
return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* List a collection's products — the MEMBERSHIP join (multi-collection
|
|
110
|
+
* products appear in every collection they belong to), published products
|
|
111
|
+
* only, ordered by the collection's `default_sort` unless `order` overrides
|
|
112
|
+
* (manual position, title, EUR price asc/desc, newest/oldest, best_selling
|
|
113
|
+
* 90-day aggregate). Products carry the full `StoreProduct` shape incl.
|
|
114
|
+
* `calculated_price` when a pricing context is given.
|
|
115
|
+
*
|
|
116
|
+
* Auth: anon (`x-client-id`); optional Bearer JWT for group-aware pricing.
|
|
117
|
+
* Errors: 404 `not_found` (unknown collection, or scoped away from
|
|
118
|
+
* `sales_channel_id`), 400 `validation_failed`, 400 `invalid_region`.
|
|
119
|
+
* `limit` 1–100 (default 50). Settings: collection default_sort + manual
|
|
120
|
+
* order, channel links, price lists.
|
|
121
|
+
*/
|
|
122
|
+
export async function listCollectionProducts(
|
|
123
|
+
client: StorefrontClient,
|
|
124
|
+
collectionId: string,
|
|
125
|
+
query?: ListCollectionProductsQuery
|
|
126
|
+
): Promise<CollectionProductsResponse> {
|
|
127
|
+
return client.get(`/api/store/collections/${encodeURIComponent(collectionId)}/products`, {
|
|
128
|
+
query: { ...query },
|
|
129
|
+
})
|
|
130
|
+
}
|
package/src/api/consent.ts
CHANGED
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/consent — the store's CMP configuration for the
|
|
3
|
-
* built-in Consent Mode v2 banner (consent-management card).
|
|
4
|
-
*
|
|
5
|
-
* Ground truth: src/app/api/store/consent/route.ts +
|
|
6
|
-
* src/lib/consent/model.ts (publicConsentPayload — exact allowlist).
|
|
7
|
-
*
|
|
8
|
-
* WIRING TRAP (the one that matters): the synchronous Consent Mode v2
|
|
9
|
-
* DEFAULT (`<ConsentInit>`, first child of <body>) must NEVER wait on this
|
|
10
|
-
* fetch — resolve the config server-side (RSC) and inline it. Render the
|
|
11
|
-
* built-in banner only when `enabled && mode === "builtin"`. In `external`
|
|
12
|
-
* mode the merchant's CMP owns the UI and must write the same `_1c_consent`
|
|
13
|
-
* cookie (or call `setConsent()`) — all Cartbase-side tag gating works off
|
|
14
|
-
* that one seam. Choices persist 12 months.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { StorefrontClient } from "./http"
|
|
18
|
-
|
|
19
|
-
/** Per-locale banner copy — every field present (defaults merged server-side). */
|
|
20
|
-
export interface ConsentCopy {
|
|
21
|
-
// Layer 1
|
|
22
|
-
title: string
|
|
23
|
-
body: string
|
|
24
|
-
privacy_link_label: string
|
|
25
|
-
accept_label: string
|
|
26
|
-
settings_label: string
|
|
27
|
-
reject_label: string
|
|
28
|
-
// Layer 2 (preferences)
|
|
29
|
-
settings_title: string
|
|
30
|
-
accept_all_label: string
|
|
31
|
-
save_label: string
|
|
32
|
-
reject_all_label: string
|
|
33
|
-
necessary_label: string
|
|
34
|
-
necessary_description: string
|
|
35
|
-
analytics_label: string
|
|
36
|
-
analytics_description: string
|
|
37
|
-
ads_label: string
|
|
38
|
-
ads_description: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The EXACT public payload — an allowlist; nothing else will ever appear.
|
|
43
|
-
* Defaults are ALWAYS applied server-side, so this is complete and
|
|
44
|
-
* renderable even for an unconfigured store (BG + EN copy shipped).
|
|
45
|
-
*/
|
|
46
|
-
export interface ConsentConfig {
|
|
47
|
-
enabled: boolean
|
|
48
|
-
/** `builtin` = Cartbase renders the banner; `external` = merchant CMP owns UI. */
|
|
49
|
-
mode: "builtin" | "external"
|
|
50
|
-
/** `modal` = blocking centered modal; `banner-bottom` = non-blocking bar. */
|
|
51
|
-
layout: "modal" | "banner-bottom"
|
|
52
|
-
/** Storefront-relative privacy/cookies page link. */
|
|
53
|
-
privacy_href: string
|
|
54
|
-
/** Show a reject-all button on the first layer. */
|
|
55
|
-
reject_on_first_layer: boolean
|
|
56
|
-
/** Keyed by locale code (at least `bg` + `en`). */
|
|
57
|
-
copy: Record<string, ConsentCopy>
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface ConsentResponse {
|
|
61
|
-
consent: ConsentConfig
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* GET /api/store/consent — the complete, renderable consent configuration.
|
|
66
|
-
*
|
|
67
|
-
* Auth: anon (x-client-id).
|
|
68
|
-
* Errors: 400 missing_client_id only — the payload always succeeds
|
|
69
|
-
* (corrupt/missing config degrades to defaults, never to a broken banner).
|
|
70
|
-
* Settings: admin → Settings → Consent (enabled/mode/layout/copy);
|
|
71
|
-
* `integrations.tracking.consent_required` mirrors `enabled`.
|
|
72
|
-
*/
|
|
73
|
-
export async function getConsent(client: StorefrontClient): Promise<ConsentResponse> {
|
|
74
|
-
return client.get("/api/store/consent")
|
|
75
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/consent — the store's CMP configuration for the
|
|
3
|
+
* built-in Consent Mode v2 banner (consent-management card).
|
|
4
|
+
*
|
|
5
|
+
* Ground truth: src/app/api/store/consent/route.ts +
|
|
6
|
+
* src/lib/consent/model.ts (publicConsentPayload — exact allowlist).
|
|
7
|
+
*
|
|
8
|
+
* WIRING TRAP (the one that matters): the synchronous Consent Mode v2
|
|
9
|
+
* DEFAULT (`<ConsentInit>`, first child of <body>) must NEVER wait on this
|
|
10
|
+
* fetch — resolve the config server-side (RSC) and inline it. Render the
|
|
11
|
+
* built-in banner only when `enabled && mode === "builtin"`. In `external`
|
|
12
|
+
* mode the merchant's CMP owns the UI and must write the same `_1c_consent`
|
|
13
|
+
* cookie (or call `setConsent()`) — all Cartbase-side tag gating works off
|
|
14
|
+
* that one seam. Choices persist 12 months.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { StorefrontClient } from "./http"
|
|
18
|
+
|
|
19
|
+
/** Per-locale banner copy — every field present (defaults merged server-side). */
|
|
20
|
+
export interface ConsentCopy {
|
|
21
|
+
// Layer 1
|
|
22
|
+
title: string
|
|
23
|
+
body: string
|
|
24
|
+
privacy_link_label: string
|
|
25
|
+
accept_label: string
|
|
26
|
+
settings_label: string
|
|
27
|
+
reject_label: string
|
|
28
|
+
// Layer 2 (preferences)
|
|
29
|
+
settings_title: string
|
|
30
|
+
accept_all_label: string
|
|
31
|
+
save_label: string
|
|
32
|
+
reject_all_label: string
|
|
33
|
+
necessary_label: string
|
|
34
|
+
necessary_description: string
|
|
35
|
+
analytics_label: string
|
|
36
|
+
analytics_description: string
|
|
37
|
+
ads_label: string
|
|
38
|
+
ads_description: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The EXACT public payload — an allowlist; nothing else will ever appear.
|
|
43
|
+
* Defaults are ALWAYS applied server-side, so this is complete and
|
|
44
|
+
* renderable even for an unconfigured store (BG + EN copy shipped).
|
|
45
|
+
*/
|
|
46
|
+
export interface ConsentConfig {
|
|
47
|
+
enabled: boolean
|
|
48
|
+
/** `builtin` = Cartbase renders the banner; `external` = merchant CMP owns UI. */
|
|
49
|
+
mode: "builtin" | "external"
|
|
50
|
+
/** `modal` = blocking centered modal; `banner-bottom` = non-blocking bar. */
|
|
51
|
+
layout: "modal" | "banner-bottom"
|
|
52
|
+
/** Storefront-relative privacy/cookies page link. */
|
|
53
|
+
privacy_href: string
|
|
54
|
+
/** Show a reject-all button on the first layer. */
|
|
55
|
+
reject_on_first_layer: boolean
|
|
56
|
+
/** Keyed by locale code (at least `bg` + `en`). */
|
|
57
|
+
copy: Record<string, ConsentCopy>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ConsentResponse {
|
|
61
|
+
consent: ConsentConfig
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* GET /api/store/consent — the complete, renderable consent configuration.
|
|
66
|
+
*
|
|
67
|
+
* Auth: anon (x-client-id).
|
|
68
|
+
* Errors: 400 missing_client_id only — the payload always succeeds
|
|
69
|
+
* (corrupt/missing config degrades to defaults, never to a broken banner).
|
|
70
|
+
* Settings: admin → Settings → Consent (enabled/mode/layout/copy);
|
|
71
|
+
* `integrations.tracking.consent_required` mirrors `enabled`.
|
|
72
|
+
*/
|
|
73
|
+
export async function getConsent(client: StorefrontClient): Promise<ConsentResponse> {
|
|
74
|
+
return client.get("/api/store/consent")
|
|
75
|
+
}
|
package/src/api/content.ts
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront/api/content — pages + blogs (content-pages-blog card).
|
|
3
|
-
*
|
|
4
|
-
* Ground truth: src/app/api/store/pages/[handle]/route.ts,
|
|
5
|
-
* src/app/api/store/blogs/[handle]/posts{,/[postHandle]}/route.ts,
|
|
6
|
-
* src/lib/content/article-shape.ts.
|
|
7
|
-
*
|
|
8
|
-
* Shapes are Shopify Storefront `Page` / `Article` verbatim so storefront
|
|
9
|
-
* code ports 1:1. PUBLISHED only — drafts and deleted rows 404 (lib filter
|
|
10
|
-
* AND anon RLS). Stored HTML is server-sanitized on write — safe to render
|
|
11
|
-
* raw (`dangerouslySetInnerHTML`).
|
|
12
|
-
*
|
|
13
|
-
* Path convention (src/lib/content/paths.ts, shared with menus/redirects):
|
|
14
|
-
* `/pages/<handle>`, `/blogs/<handle>`, `/blogs/<blog>/<post>`.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { StorefrontClient } from "./http"
|
|
18
|
-
import type { IsoDateString, PaginationQuery } from "./types"
|
|
19
|
-
|
|
20
|
-
/** Shopify Storefront `Page`, verbatim. */
|
|
21
|
-
export interface Page {
|
|
22
|
-
/** `cpage_<hex>` */
|
|
23
|
-
id: string
|
|
24
|
-
handle: string
|
|
25
|
-
title: string
|
|
26
|
-
/** Sanitized HTML — safe to render raw. */
|
|
27
|
-
body: string
|
|
28
|
-
/** Stripped text, ~160-char word-boundary truncation. */
|
|
29
|
-
bodySummary: string
|
|
30
|
-
/** Fallbacks applied server-side: title / bodySummary. */
|
|
31
|
-
seo: { title: string; description: string }
|
|
32
|
-
publishedAt: IsoDateString
|
|
33
|
-
createdAt: IsoDateString
|
|
34
|
-
updatedAt: IsoDateString
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface PageResponse {
|
|
38
|
-
page: Page
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* GET /api/store/pages/:handle — one published page.
|
|
43
|
-
*
|
|
44
|
-
* Auth: anon (x-client-id).
|
|
45
|
-
* Errors: 404 not_found (unknown handle, draft, or deleted).
|
|
46
|
-
* Settings: policy pages (privacy-policy, terms-of-service, refund-policy,
|
|
47
|
-
* shipping-policy) are seeded as DRAFTS — they 404 until published.
|
|
48
|
-
*/
|
|
49
|
-
export async function getPage(
|
|
50
|
-
client: StorefrontClient,
|
|
51
|
-
handle: string
|
|
52
|
-
): Promise<PageResponse> {
|
|
53
|
-
return client.get(`/api/store/pages/${encodeURIComponent(handle)}`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Shopify Storefront `Article`, verbatim. */
|
|
57
|
-
export interface Article {
|
|
58
|
-
/** `bpost_<hex>` */
|
|
59
|
-
id: string
|
|
60
|
-
handle: string
|
|
61
|
-
title: string
|
|
62
|
-
/** Sanitized HTML — safe to render raw. */
|
|
63
|
-
contentHtml: string
|
|
64
|
-
/** Plain-text version of contentHtml. */
|
|
65
|
-
content: string
|
|
66
|
-
/** Author-provided; falls back to truncated content. */
|
|
67
|
-
excerpt: string
|
|
68
|
-
/** null when the post has no hero image. altText is always null today. */
|
|
69
|
-
image: { url: string; altText: null } | null
|
|
70
|
-
tags: string[]
|
|
71
|
-
/** null when the post has no author name set. */
|
|
72
|
-
author: { name: string } | null
|
|
73
|
-
publishedAt: IsoDateString
|
|
74
|
-
/** Fallbacks applied server-side: title / excerpt. */
|
|
75
|
-
seo: { title: string; description: string }
|
|
76
|
-
blog: { handle: string; title: string }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ArticleResponse {
|
|
80
|
-
article: Article
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export type ListBlogPostsQuery = PaginationQuery
|
|
84
|
-
|
|
85
|
-
export interface BlogPostListResponse {
|
|
86
|
-
blog: { handle: string; title: string }
|
|
87
|
-
articles: Article[]
|
|
88
|
-
count: number
|
|
89
|
-
offset: number
|
|
90
|
-
limit: number
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* GET /api/store/blogs/:handle/posts — published posts of one blog,
|
|
95
|
-
* `published_at` desc. `limit` ≤ 100 (default 20), `offset` ≥ 0.
|
|
96
|
-
*
|
|
97
|
-
* Auth: anon (x-client-id).
|
|
98
|
-
* Errors: 404 not_found (unknown blog handle) · 400 validation_failed
|
|
99
|
-
* (limit/offset out of range).
|
|
100
|
-
*/
|
|
101
|
-
export async function listBlogPosts(
|
|
102
|
-
client: StorefrontClient,
|
|
103
|
-
blogHandle: string,
|
|
104
|
-
query?: ListBlogPostsQuery
|
|
105
|
-
): Promise<BlogPostListResponse> {
|
|
106
|
-
return client.get(`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts`, {
|
|
107
|
-
query: { ...query },
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* GET /api/store/blogs/:handle/posts/:postHandle — one published article.
|
|
113
|
-
*
|
|
114
|
-
* Auth: anon (x-client-id).
|
|
115
|
-
* Errors: 404 not_found (unknown blog OR unknown/draft post).
|
|
116
|
-
*/
|
|
117
|
-
export async function getBlogPost(
|
|
118
|
-
client: StorefrontClient,
|
|
119
|
-
blogHandle: string,
|
|
120
|
-
postHandle: string
|
|
121
|
-
): Promise<ArticleResponse> {
|
|
122
|
-
return client.get(
|
|
123
|
-
`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts/${encodeURIComponent(postHandle)}`
|
|
124
|
-
)
|
|
125
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/content — pages + blogs (content-pages-blog card).
|
|
3
|
+
*
|
|
4
|
+
* Ground truth: src/app/api/store/pages/[handle]/route.ts,
|
|
5
|
+
* src/app/api/store/blogs/[handle]/posts{,/[postHandle]}/route.ts,
|
|
6
|
+
* src/lib/content/article-shape.ts.
|
|
7
|
+
*
|
|
8
|
+
* Shapes are Shopify Storefront `Page` / `Article` verbatim so storefront
|
|
9
|
+
* code ports 1:1. PUBLISHED only — drafts and deleted rows 404 (lib filter
|
|
10
|
+
* AND anon RLS). Stored HTML is server-sanitized on write — safe to render
|
|
11
|
+
* raw (`dangerouslySetInnerHTML`).
|
|
12
|
+
*
|
|
13
|
+
* Path convention (src/lib/content/paths.ts, shared with menus/redirects):
|
|
14
|
+
* `/pages/<handle>`, `/blogs/<handle>`, `/blogs/<blog>/<post>`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { StorefrontClient } from "./http"
|
|
18
|
+
import type { IsoDateString, PaginationQuery } from "./types"
|
|
19
|
+
|
|
20
|
+
/** Shopify Storefront `Page`, verbatim. */
|
|
21
|
+
export interface Page {
|
|
22
|
+
/** `cpage_<hex>` */
|
|
23
|
+
id: string
|
|
24
|
+
handle: string
|
|
25
|
+
title: string
|
|
26
|
+
/** Sanitized HTML — safe to render raw. */
|
|
27
|
+
body: string
|
|
28
|
+
/** Stripped text, ~160-char word-boundary truncation. */
|
|
29
|
+
bodySummary: string
|
|
30
|
+
/** Fallbacks applied server-side: title / bodySummary. */
|
|
31
|
+
seo: { title: string; description: string }
|
|
32
|
+
publishedAt: IsoDateString
|
|
33
|
+
createdAt: IsoDateString
|
|
34
|
+
updatedAt: IsoDateString
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PageResponse {
|
|
38
|
+
page: Page
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* GET /api/store/pages/:handle — one published page.
|
|
43
|
+
*
|
|
44
|
+
* Auth: anon (x-client-id).
|
|
45
|
+
* Errors: 404 not_found (unknown handle, draft, or deleted).
|
|
46
|
+
* Settings: policy pages (privacy-policy, terms-of-service, refund-policy,
|
|
47
|
+
* shipping-policy) are seeded as DRAFTS — they 404 until published.
|
|
48
|
+
*/
|
|
49
|
+
export async function getPage(
|
|
50
|
+
client: StorefrontClient,
|
|
51
|
+
handle: string
|
|
52
|
+
): Promise<PageResponse> {
|
|
53
|
+
return client.get(`/api/store/pages/${encodeURIComponent(handle)}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Shopify Storefront `Article`, verbatim. */
|
|
57
|
+
export interface Article {
|
|
58
|
+
/** `bpost_<hex>` */
|
|
59
|
+
id: string
|
|
60
|
+
handle: string
|
|
61
|
+
title: string
|
|
62
|
+
/** Sanitized HTML — safe to render raw. */
|
|
63
|
+
contentHtml: string
|
|
64
|
+
/** Plain-text version of contentHtml. */
|
|
65
|
+
content: string
|
|
66
|
+
/** Author-provided; falls back to truncated content. */
|
|
67
|
+
excerpt: string
|
|
68
|
+
/** null when the post has no hero image. altText is always null today. */
|
|
69
|
+
image: { url: string; altText: null } | null
|
|
70
|
+
tags: string[]
|
|
71
|
+
/** null when the post has no author name set. */
|
|
72
|
+
author: { name: string } | null
|
|
73
|
+
publishedAt: IsoDateString
|
|
74
|
+
/** Fallbacks applied server-side: title / excerpt. */
|
|
75
|
+
seo: { title: string; description: string }
|
|
76
|
+
blog: { handle: string; title: string }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ArticleResponse {
|
|
80
|
+
article: Article
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ListBlogPostsQuery = PaginationQuery
|
|
84
|
+
|
|
85
|
+
export interface BlogPostListResponse {
|
|
86
|
+
blog: { handle: string; title: string }
|
|
87
|
+
articles: Article[]
|
|
88
|
+
count: number
|
|
89
|
+
offset: number
|
|
90
|
+
limit: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GET /api/store/blogs/:handle/posts — published posts of one blog,
|
|
95
|
+
* `published_at` desc. `limit` ≤ 100 (default 20), `offset` ≥ 0.
|
|
96
|
+
*
|
|
97
|
+
* Auth: anon (x-client-id).
|
|
98
|
+
* Errors: 404 not_found (unknown blog handle) · 400 validation_failed
|
|
99
|
+
* (limit/offset out of range).
|
|
100
|
+
*/
|
|
101
|
+
export async function listBlogPosts(
|
|
102
|
+
client: StorefrontClient,
|
|
103
|
+
blogHandle: string,
|
|
104
|
+
query?: ListBlogPostsQuery
|
|
105
|
+
): Promise<BlogPostListResponse> {
|
|
106
|
+
return client.get(`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts`, {
|
|
107
|
+
query: { ...query },
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GET /api/store/blogs/:handle/posts/:postHandle — one published article.
|
|
113
|
+
*
|
|
114
|
+
* Auth: anon (x-client-id).
|
|
115
|
+
* Errors: 404 not_found (unknown blog OR unknown/draft post).
|
|
116
|
+
*/
|
|
117
|
+
export async function getBlogPost(
|
|
118
|
+
client: StorefrontClient,
|
|
119
|
+
blogHandle: string,
|
|
120
|
+
postHandle: string
|
|
121
|
+
): Promise<ArticleResponse> {
|
|
122
|
+
return client.get(
|
|
123
|
+
`/api/store/blogs/${encodeURIComponent(blogHandle)}/posts/${encodeURIComponent(postHandle)}`
|
|
124
|
+
)
|
|
125
|
+
}
|