@cartbase/storefront 0.1.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.
Files changed (176) hide show
  1. package/package.json +226 -0
  2. package/src/api/auth.ts +108 -0
  3. package/src/api/carts.ts +506 -0
  4. package/src/api/categories.ts +184 -0
  5. package/src/api/checkout.ts +440 -0
  6. package/src/api/collections.ts +130 -0
  7. package/src/api/consent.ts +75 -0
  8. package/src/api/content.ts +125 -0
  9. package/src/api/customers.ts +307 -0
  10. package/src/api/gift-cards.ts +112 -0
  11. package/src/api/http.ts +122 -0
  12. package/src/api/index.ts +29 -0
  13. package/src/api/integrations.ts +130 -0
  14. package/src/api/menus.ts +77 -0
  15. package/src/api/metaobjects.ts +136 -0
  16. package/src/api/orders.ts +290 -0
  17. package/src/api/products.ts +303 -0
  18. package/src/api/redirects.ts +37 -0
  19. package/src/api/regions.ts +141 -0
  20. package/src/api/reviews.ts +259 -0
  21. package/src/api/search.ts +133 -0
  22. package/src/api/types.ts +91 -0
  23. package/src/cart-drawer/cart-drawer.tsx +86 -0
  24. package/src/cart-drawer/context.tsx +569 -0
  25. package/src/cart-drawer/continue-shopping.tsx +28 -0
  26. package/src/cart-drawer/cross-sell-carousel.tsx +212 -0
  27. package/src/cart-drawer/cross-sell-sidebar.tsx +158 -0
  28. package/src/cart-drawer/empty.tsx +52 -0
  29. package/src/cart-drawer/free-gift.tsx +71 -0
  30. package/src/cart-drawer/gift-wrap.tsx +83 -0
  31. package/src/cart-drawer/header.tsx +52 -0
  32. package/src/cart-drawer/index.ts +69 -0
  33. package/src/cart-drawer/item/index.tsx +164 -0
  34. package/src/cart-drawer/item/quantity.tsx +100 -0
  35. package/src/cart-drawer/item/upsell.tsx +110 -0
  36. package/src/cart-drawer/item/variant.tsx +46 -0
  37. package/src/cart-drawer/labels-bg.ts +72 -0
  38. package/src/cart-drawer/labels.ts +119 -0
  39. package/src/cart-drawer/notes.tsx +131 -0
  40. package/src/cart-drawer/payment-badges.tsx +96 -0
  41. package/src/cart-drawer/promo-banner.tsx +43 -0
  42. package/src/cart-drawer/rewards-points.tsx +78 -0
  43. package/src/cart-drawer/sticky-footer.tsx +73 -0
  44. package/src/cart-drawer/summary-breakdown.tsx +196 -0
  45. package/src/cart-drawer/template.tsx +225 -0
  46. package/src/cart-drawer/tiered-progress.tsx +168 -0
  47. package/src/checkout/address-error-copy.ts +119 -0
  48. package/src/checkout/address-form.tsx +224 -0
  49. package/src/checkout/address-select.tsx +79 -0
  50. package/src/checkout/boxnow-locker-selector.tsx +410 -0
  51. package/src/checkout/checkout-client.tsx +222 -0
  52. package/src/checkout/company-details.tsx +94 -0
  53. package/src/checkout/compare-addresses.ts +40 -0
  54. package/src/checkout/context.tsx +76 -0
  55. package/src/checkout/discount-section.tsx +218 -0
  56. package/src/checkout/econt-office-selector.tsx +332 -0
  57. package/src/checkout/error-message.tsx +25 -0
  58. package/src/checkout/geocode.ts +154 -0
  59. package/src/checkout/gift-card-section.tsx +224 -0
  60. package/src/checkout/index.ts +74 -0
  61. package/src/checkout/labels-bg.ts +128 -0
  62. package/src/checkout/labels.ts +263 -0
  63. package/src/checkout/line-item-card.tsx +152 -0
  64. package/src/checkout/order-summary.tsx +524 -0
  65. package/src/checkout/payment-button.tsx +373 -0
  66. package/src/checkout/payment-error-copy.ts +269 -0
  67. package/src/checkout/payment-method-list.tsx +365 -0
  68. package/src/checkout/payment-wrapper.tsx +102 -0
  69. package/src/checkout/promotion-error-copy.ts +124 -0
  70. package/src/checkout/shipping-method-list.tsx +335 -0
  71. package/src/checkout/stripe-wrapper.tsx +165 -0
  72. package/src/checkout/use-checkout-orchestration.ts +1504 -0
  73. package/src/common/cart-button-client.tsx +39 -0
  74. package/src/common/cart-button.tsx +28 -0
  75. package/src/common/country-select.tsx +65 -0
  76. package/src/common/delete-button.tsx +66 -0
  77. package/src/common/index.ts +17 -0
  78. package/src/common/language-select.tsx +78 -0
  79. package/src/common/localized-link.tsx +45 -0
  80. package/src/common/skeleton.tsx +29 -0
  81. package/src/index.ts +12 -0
  82. package/src/lib/cart-helpers.ts +113 -0
  83. package/src/lib/dual-price.tsx +73 -0
  84. package/src/lib/get-percentage-diff.ts +5 -0
  85. package/src/lib/get-product-price.ts +133 -0
  86. package/src/lib/hooks/use-intersection.ts +30 -0
  87. package/src/lib/hooks/use-toggle-state.ts +25 -0
  88. package/src/lib/money.ts +73 -0
  89. package/src/lib/payment-constants.ts +66 -0
  90. package/src/lib/product.ts +22 -0
  91. package/src/lib/sort-products.ts +63 -0
  92. package/src/lib/store-api-error.ts +36 -0
  93. package/src/lib/utils.ts +16 -0
  94. package/src/order/context.tsx +32 -0
  95. package/src/order/index.ts +63 -0
  96. package/src/order/labels-bg.ts +39 -0
  97. package/src/order/labels.ts +79 -0
  98. package/src/order/order-address-card.tsx +47 -0
  99. package/src/order/order-completed-template.tsx +165 -0
  100. package/src/order/order-confirmation-header.tsx +65 -0
  101. package/src/order/order-delivery-card.tsx +258 -0
  102. package/src/order/order-help-section.tsx +47 -0
  103. package/src/order/order-item.tsx +201 -0
  104. package/src/order/order-items-list.tsx +52 -0
  105. package/src/order/order-payment-card.tsx +95 -0
  106. package/src/order/order-timeline.tsx +141 -0
  107. package/src/order/order-totals.tsx +245 -0
  108. package/src/primitives/field.tsx +125 -0
  109. package/src/primitives/select-field.tsx +77 -0
  110. package/src/primitives/ui/accordion.tsx +61 -0
  111. package/src/primitives/ui/button.tsx +68 -0
  112. package/src/primitives/ui/collapsible.tsx +16 -0
  113. package/src/primitives/ui/dialog.tsx +112 -0
  114. package/src/primitives/ui/input.tsx +30 -0
  115. package/src/primitives/ui/label.tsx +31 -0
  116. package/src/primitives/ui/popover.tsx +38 -0
  117. package/src/primitives/ui/select.tsx +163 -0
  118. package/src/primitives/ui/sheet.tsx +131 -0
  119. package/src/primitives/ui/tabs.tsx +62 -0
  120. package/src/products/context.tsx +34 -0
  121. package/src/products/image-gallery.tsx +43 -0
  122. package/src/products/index.ts +44 -0
  123. package/src/products/labels-bg.ts +35 -0
  124. package/src/products/labels.ts +57 -0
  125. package/src/products/mobile-actions.tsx +180 -0
  126. package/src/products/option-select.tsx +67 -0
  127. package/src/products/preview-price.tsx +36 -0
  128. package/src/products/product-actions-wrapper.tsx +58 -0
  129. package/src/products/product-actions.tsx +217 -0
  130. package/src/products/product-info.tsx +43 -0
  131. package/src/products/product-preview.tsx +49 -0
  132. package/src/products/product-price.tsx +69 -0
  133. package/src/products/product-tabs.tsx +169 -0
  134. package/src/products/product-template.tsx +114 -0
  135. package/src/products/purchase-options.tsx +130 -0
  136. package/src/products/related-products.tsx +86 -0
  137. package/src/products/thumbnail.tsx +71 -0
  138. package/src/products/variant-matching.ts +71 -0
  139. package/src/reviews-ui/helpers.ts +174 -0
  140. package/src/reviews-ui/index.ts +74 -0
  141. package/src/reviews-ui/labels-bg.ts +91 -0
  142. package/src/reviews-ui/labels.ts +199 -0
  143. package/src/reviews-ui/photo-upload.tsx +345 -0
  144. package/src/reviews-ui/review-list.tsx +249 -0
  145. package/src/reviews-ui/review-widget.tsx +224 -0
  146. package/src/reviews-ui/review-wizard.tsx +560 -0
  147. package/src/reviews-ui/star-badge.tsx +104 -0
  148. package/src/reviews-ui/wizard-state.ts +81 -0
  149. package/src/store/category-template.tsx +129 -0
  150. package/src/store/collection-template.tsx +139 -0
  151. package/src/store/index.ts +41 -0
  152. package/src/store/labels-bg.ts +22 -0
  153. package/src/store/labels.ts +52 -0
  154. package/src/store/paginated-products.tsx +116 -0
  155. package/src/store/pagination.tsx +103 -0
  156. package/src/store/search-params.ts +256 -0
  157. package/src/store/search-template.tsx +249 -0
  158. package/src/store/skeleton-product-grid.tsx +26 -0
  159. package/src/store/sort-select.tsx +81 -0
  160. package/src/store/store-template.tsx +65 -0
  161. package/src/tracking/attribution.ts +418 -0
  162. package/src/tracking/consent-banner.tsx +355 -0
  163. package/src/tracking/consent-init.tsx +44 -0
  164. package/src/tracking/consent.ts +243 -0
  165. package/src/tracking/fbq.ts +168 -0
  166. package/src/tracking/ga4.tsx +49 -0
  167. package/src/tracking/get-tracking-attribution.ts +224 -0
  168. package/src/tracking/get-tracking-config.ts +50 -0
  169. package/src/tracking/gtag.ts +200 -0
  170. package/src/tracking/index.ts +133 -0
  171. package/src/tracking/meta-pixel.tsx +166 -0
  172. package/src/tracking/rybbit-events.ts +242 -0
  173. package/src/tracking/rybbit.tsx +40 -0
  174. package/src/tracking/types.ts +185 -0
  175. package/src/tracking/use-engagement-time.ts +58 -0
  176. package/tailwind-preset.cjs +72 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * @cartbase/storefront/api/customers — customer profile, addresses, documents.
3
+ *
4
+ * Ground truth: src/app/api/store/customers/** + src/app/api/_lib/customer.ts.
5
+ *
6
+ * Auth model: every endpoint here except none is CUSTOMER-authenticated —
7
+ * `authorization: Bearer <supabase jwt>` (minted by the passwordless code
8
+ * flow in ./auth or a client-side supabase password sign-in) PLUS the anon
9
+ * `x-client-id` tenant header the client always sends. Missing/invalid JWT →
10
+ * `401 unauthenticated`.
11
+ *
12
+ * Lazy registration: `/customers/me` resolves the customer row by
13
+ * (client_id, JWT email) and CREATES it on first authenticated call
14
+ * (has_account=true, account_status per the store's approval policy) — a
15
+ * fresh login never 404s on "me".
16
+ */
17
+
18
+ import type { StorefrontClient } from "./http"
19
+ import type { IsoDateString, PaginationQuery } from "./types"
20
+
21
+ /** One row of `customer.addresses` (barter_commerce.customer_addresses). */
22
+ export interface CustomerAddress {
23
+ id: string
24
+ client_id: string
25
+ customer_id: string
26
+ address_name: string | null
27
+ first_name: string | null
28
+ last_name: string | null
29
+ company: string | null
30
+ address_1: string | null
31
+ address_2: string | null
32
+ city: string | null
33
+ country_code: string | null
34
+ province: string | null
35
+ postal_code: string | null
36
+ phone: string | null
37
+ is_default_billing: boolean
38
+ is_default_shipping: boolean
39
+ metadata: Record<string, unknown> | null
40
+ created_at: IsoDateString
41
+ updated_at: IsoDateString
42
+ deleted_at: IsoDateString | null
43
+ }
44
+
45
+ /**
46
+ * The customer payload (full barter_commerce.customers row + addresses).
47
+ * `account_status` (`pending` | `approved`) gates B2B storefront content
48
+ * when the store's approval policy is on (b2b-v1). `tags` are admin-only
49
+ * labels — readable here, never writable from the store surface.
50
+ */
51
+ export interface StoreCustomer {
52
+ id: string
53
+ client_id: string
54
+ email: string | null
55
+ first_name: string | null
56
+ last_name: string | null
57
+ phone: string | null
58
+ company_name: string | null
59
+ /** Bulgarian company id (ЕИК) — feeds invoice-required checkout. */
60
+ company_eik: string | null
61
+ vat_number: string | null
62
+ has_account: boolean
63
+ /** `pending` | `approved` — store approval policy (customer-accounts card). */
64
+ account_status: string
65
+ tags: string[]
66
+ metadata: Record<string, unknown> | null
67
+ created_by: string | null
68
+ created_at: IsoDateString
69
+ updated_at: IsoDateString
70
+ deleted_at: IsoDateString | null
71
+ addresses: CustomerAddress[]
72
+ }
73
+
74
+ export interface CustomerResponse {
75
+ customer: StoreCustomer
76
+ }
77
+
78
+ export interface CreateCustomerInput {
79
+ /** Must equal the session JWT's email — mismatch → 403 email_mismatch. */
80
+ email: string
81
+ first_name?: string
82
+ last_name?: string
83
+ phone?: string
84
+ company_name?: string
85
+ }
86
+
87
+ /**
88
+ * POST /api/store/customers — guest-to-registered conversion (password
89
+ * sign-up flow): call `supabase.auth.signUp({email, password})` client-side
90
+ * first, then this with the same email. Upserts the (client_id, email)
91
+ * customer row with has_account=true. NOT needed for the passwordless code
92
+ * flow — verify already lazy-creates the customer.
93
+ *
94
+ * Auth: Bearer JWT (+ x-client-id).
95
+ * Errors: 401 unauthenticated · 403 email_mismatch (body email ≠ JWT email)
96
+ * · 400 validation_failed.
97
+ * Settings: store approval policy decides account_status of a NEW row.
98
+ */
99
+ export async function createCustomer(
100
+ client: StorefrontClient,
101
+ input: CreateCustomerInput
102
+ ): Promise<CustomerResponse> {
103
+ return client.post("/api/store/customers", input)
104
+ }
105
+
106
+ /**
107
+ * GET /api/store/customers/me — the signed-in customer, addresses embedded.
108
+ * Lazy-creates the customer row on first call for this (tenant, email).
109
+ *
110
+ * Auth: Bearer JWT (+ x-client-id).
111
+ * Errors: 401 unauthenticated · 400 missing_client_id.
112
+ */
113
+ export async function getMe(client: StorefrontClient): Promise<CustomerResponse> {
114
+ return client.get("/api/store/customers/me")
115
+ }
116
+
117
+ export interface UpdateCustomerInput {
118
+ first_name?: string | null
119
+ last_name?: string | null
120
+ phone?: string | null
121
+ company_name?: string | null
122
+ /** ЕИК / VAT (customer-master-data) — loose format, never shape-blocked. */
123
+ company_eik?: string | null
124
+ vat_number?: string | null
125
+ metadata?: Record<string, unknown>
126
+ // NOTE: `tags` deliberately absent — admin-only; the server strips them.
127
+ }
128
+
129
+ /**
130
+ * POST /api/store/customers/me — update own profile. Unknown keys (incl.
131
+ * `tags`) are stripped server-side; returns the refreshed customer.
132
+ *
133
+ * Auth: Bearer JWT (+ x-client-id).
134
+ * Errors: 401 unauthenticated · 400 validation_failed.
135
+ */
136
+ export async function updateMe(
137
+ client: StorefrontClient,
138
+ input: UpdateCustomerInput
139
+ ): Promise<CustomerResponse> {
140
+ return client.post("/api/store/customers/me", input)
141
+ }
142
+
143
+ export interface AddressListResponse {
144
+ addresses: CustomerAddress[]
145
+ /** Not truly paginated: offset is always 0 and limit === count. */
146
+ count: number
147
+ offset: number
148
+ limit: number
149
+ }
150
+
151
+ /**
152
+ * GET /api/store/customers/me/addresses — all of the customer's addresses
153
+ * (created_at asc). The list envelope is nominal — the route returns EVERY
154
+ * address (offset 0, limit = count); no query params are read.
155
+ *
156
+ * Auth: Bearer JWT (+ x-client-id). Errors: 401 unauthenticated.
157
+ */
158
+ export async function listAddresses(
159
+ client: StorefrontClient
160
+ ): Promise<AddressListResponse> {
161
+ return client.get("/api/store/customers/me/addresses")
162
+ }
163
+
164
+ export interface CreateAddressInput {
165
+ address_name?: string
166
+ first_name?: string
167
+ last_name?: string
168
+ company?: string
169
+ address_1?: string
170
+ address_2?: string
171
+ city?: string
172
+ /** Lower-cased server-side. */
173
+ country_code?: string
174
+ province?: string
175
+ postal_code?: string
176
+ phone?: string
177
+ /** true clears the flag on every sibling first (one default per kind). */
178
+ is_default_billing?: boolean
179
+ is_default_shipping?: boolean
180
+ metadata?: Record<string, unknown>
181
+ }
182
+
183
+ /**
184
+ * POST /api/store/customers/me/addresses — create an address. Returns the
185
+ * FULL refreshed customer (`{customer}`, addresses embedded) — not the
186
+ * created address alone; find it in `customer.addresses`.
187
+ *
188
+ * Auth: Bearer JWT (+ x-client-id).
189
+ * Errors: 401 unauthenticated · 400 validation_failed.
190
+ */
191
+ export async function createAddress(
192
+ client: StorefrontClient,
193
+ input: CreateAddressInput
194
+ ): Promise<CustomerResponse> {
195
+ return client.post("/api/store/customers/me/addresses", input)
196
+ }
197
+
198
+ export interface AddressResponse {
199
+ address: CustomerAddress
200
+ }
201
+
202
+ /**
203
+ * GET /api/store/customers/me/addresses/:id — one owned address.
204
+ *
205
+ * Auth: Bearer JWT (+ x-client-id).
206
+ * Errors: 401 unauthenticated · 404 not_found (unknown OR another
207
+ * customer's address — ownership 404s, it never 403s).
208
+ */
209
+ export async function getAddress(
210
+ client: StorefrontClient,
211
+ addressId: string
212
+ ): Promise<AddressResponse> {
213
+ return client.get(`/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`)
214
+ }
215
+
216
+ export interface UpdateAddressInput {
217
+ address_name?: string | null
218
+ first_name?: string | null
219
+ last_name?: string | null
220
+ company?: string | null
221
+ address_1?: string | null
222
+ address_2?: string | null
223
+ city?: string | null
224
+ country_code?: string | null
225
+ province?: string | null
226
+ postal_code?: string | null
227
+ phone?: string | null
228
+ is_default_billing?: boolean
229
+ is_default_shipping?: boolean
230
+ metadata?: Record<string, unknown>
231
+ }
232
+
233
+ /**
234
+ * POST /api/store/customers/me/addresses/:id — partial update; default
235
+ * flags clear siblings. Returns the FULL refreshed customer.
236
+ *
237
+ * Auth: Bearer JWT (+ x-client-id).
238
+ * Errors: 401 unauthenticated · 404 not_found · 400 validation_failed.
239
+ */
240
+ export async function updateAddress(
241
+ client: StorefrontClient,
242
+ addressId: string,
243
+ input: UpdateAddressInput
244
+ ): Promise<CustomerResponse> {
245
+ return client.post(
246
+ `/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`,
247
+ input
248
+ )
249
+ }
250
+
251
+ /**
252
+ * DELETE /api/store/customers/me/addresses/:id — soft-delete an owned
253
+ * address. Returns the FULL refreshed customer (the address gone from
254
+ * `customer.addresses`).
255
+ *
256
+ * Auth: Bearer JWT (+ x-client-id).
257
+ * Errors: 401 unauthenticated · 404 not_found.
258
+ */
259
+ export async function deleteAddress(
260
+ client: StorefrontClient,
261
+ addressId: string
262
+ ): Promise<CustomerResponse> {
263
+ return client.delete(
264
+ `/api/store/customers/me/addresses/${encodeURIComponent(addressId)}`
265
+ )
266
+ }
267
+
268
+ /** One issued (non-void) order document — the account "Invoices" row. */
269
+ export interface CustomerDocument {
270
+ id: string
271
+ order_id: string
272
+ order_display_id: number | null
273
+ doc_type: string
274
+ number: string
275
+ issued_at: IsoDateString
276
+ pdf_url: string | null
277
+ due_date: IsoDateString | null
278
+ paid_at: IsoDateString | null
279
+ }
280
+
281
+ export interface ListDocumentsQuery extends PaginationQuery {
282
+ /** Filter by document type (e.g. `invoice`). */
283
+ doc_type?: string
284
+ }
285
+
286
+ export interface DocumentListResponse {
287
+ documents: CustomerDocument[]
288
+ count: number
289
+ offset: number
290
+ limit: number
291
+ }
292
+
293
+ /**
294
+ * GET /api/store/customers/me/documents — the customer's issued order
295
+ * documents, newest first (issued_at desc). Voided documents excluded.
296
+ * Ownership is INNER-join enforced (session customer id AND client_id) —
297
+ * cross-customer reads are impossible. `limit` ≤ 200, default 20.
298
+ *
299
+ * Auth: Bearer JWT (+ x-client-id).
300
+ * Errors: 401 unauthenticated · 400 validation_failed.
301
+ */
302
+ export async function listMyDocuments(
303
+ client: StorefrontClient,
304
+ query?: ListDocumentsQuery
305
+ ): Promise<DocumentListResponse> {
306
+ return client.get("/api/store/customers/me/documents", { query: { ...query } })
307
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @cartbase/storefront/api/gift-cards — gift-card TENDER on carts.
3
+ *
4
+ * Redemption is a payment tender (`pp_giftcard`), never a discount: cart
5
+ * totals/VAT compute first and NEVER move; applied cards cover part (or all)
6
+ * of `cart.total` and the remainder provider (Stripe/COD/manual) charges
7
+ * only what is left. The cart decoration fields (`gift_cards[]`,
8
+ * `gift_card_total`, `gift_card_remainder` — declared on the `Cart` DTO in
9
+ * `./carts`) are derived from the LIVE transactions ledger at every read.
10
+ *
11
+ * Route ground truth: src/app/api/store/carts/[id]/gift-cards/route.ts +
12
+ * src/lib/gift-cards/{redeem,tender}.ts. Doc: docs/storefront/gift-cards.md.
13
+ *
14
+ * Security model baked into the contract:
15
+ * - failure is ONE generic answer — unknown, disabled, expired, depleted
16
+ * and foreign-tenant codes are all `400 invalid_gift_card` (no
17
+ * code-existence oracle);
18
+ * - attempts are recorded BEFORE lookup and rate-limited per cart
19
+ * (10 / 15 min) AND per IP (30 / 15 min) → `429 rate_limited`;
20
+ * - the confirmation is MASKED (`last4` only) — the code is never echoed.
21
+ */
22
+ import type { StorefrontClient } from "./http"
23
+ import type { MajorUnitAmount } from "./types"
24
+ import type { Cart } from "./carts"
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // DTOs
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** Body of POST /api/store/carts/:id/gift-cards (`.strict()`). */
31
+ export interface ApplyGiftCardInput {
32
+ /** The plaintext code (4–64 chars). Hashed at rest server-side. */
33
+ code: string
34
+ }
35
+
36
+ /** Body of DELETE /api/store/carts/:id/gift-cards (`.strict()`). */
37
+ export interface RemoveGiftCardInput {
38
+ /** The applied card's id (from `cart.gift_cards[].id` or the apply response). */
39
+ gift_card_id: string
40
+ }
41
+
42
+ /** Masked apply confirmation. */
43
+ export interface AppliedGiftCardConfirmation {
44
+ id: string
45
+ /** Last 4 characters of the code — the full code is never echoed. */
46
+ last4: string
47
+ /**
48
+ * How much of the cart total THIS card covers right now
49
+ * (`min(live balance, remaining total)` in apply order).
50
+ */
51
+ amount_applied: MajorUnitAmount
52
+ }
53
+
54
+ export interface ApplyGiftCardResponse {
55
+ /** Decorated cart — `gift_cards[]` / `gift_card_total` / `gift_card_remainder` updated; `total` untouched. */
56
+ cart: Cart
57
+ gift_card: AppliedGiftCardConfirmation
58
+ }
59
+
60
+ export interface RemoveGiftCardResponse {
61
+ cart: Cart
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Functions
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Apply a gift-card code to an open cart (idempotent re-apply). → 200
70
+ * `{cart, gift_card}` with a MASKED confirmation.
71
+ *
72
+ * Auth: anon `x-client-id`.
73
+ * Errors: 400 `invalid_gift_card` (the deliberate generic answer for
74
+ * unknown/disabled/expired/depleted/foreign-tenant codes), 429
75
+ * `rate_limited` (per-cart and per-IP attempt windows — a valid code inside
76
+ * a burned window is refused the same way), 404 `cart_not_found`, 409
77
+ * `cart_completed`, 400 `validation_failed`.
78
+ *
79
+ * Behavior: also syncs the internal `pp_giftcard` payment session to the
80
+ * covered amount when a payment collection exists (no-op before one does —
81
+ * creating the collection composes it). Actual balance redemption happens
82
+ * ONLY at cart complete, atomically; a card drained elsewhere in the
83
+ * meantime shrinks this cart's tender at the next read instead of
84
+ * over-redeeming.
85
+ * Settings: gift cards are issued/disabled from the admin; expiry and
86
+ * balance live on the card's ledger.
87
+ */
88
+ export async function applyGiftCard(
89
+ client: StorefrontClient,
90
+ cartId: string,
91
+ input: ApplyGiftCardInput
92
+ ): Promise<ApplyGiftCardResponse> {
93
+ return client.post(`/api/store/carts/${cartId}/gift-cards`, input)
94
+ }
95
+
96
+ /**
97
+ * Remove an applied gift card from an open cart (idempotent). → 200 `{cart}`.
98
+ *
99
+ * Auth: anon `x-client-id`.
100
+ * Errors: 404 `cart_not_found`, 409 `cart_completed`, 400
101
+ * `validation_failed`.
102
+ * Behavior: re-syncs the `pp_giftcard` session to the remaining tender
103
+ * (removing the last card zeroes it), so the remainder provider's session
104
+ * re-inflates to the full total on the next amount sync.
105
+ */
106
+ export async function removeGiftCard(
107
+ client: StorefrontClient,
108
+ cartId: string,
109
+ input: RemoveGiftCardInput
110
+ ): Promise<RemoveGiftCardResponse> {
111
+ return client.delete(`/api/store/carts/${cartId}/gift-cards`, input)
112
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @cartbase/storefront — the ONE http seam.
3
+ *
4
+ * Every SDK function in this package is a thin typed wrapper over
5
+ * `storeFetch()` from a `StorefrontClient`. Auth model (store-api.md):
6
+ *
7
+ * - `x-client-id` — required for anon reads (RLS scope)
8
+ * - `x-publishable-api-key` — optional; scopes catalog/carts to the key's
9
+ * sales channels when set
10
+ * - `authorization: Bearer <jwt>` — customer session (passwordless code
11
+ * flow or supabase password login)
12
+ * - `x-locale` — optional storefront locale hint
13
+ *
14
+ * The client is isomorphic (browser + RSC/server actions) — it holds no
15
+ * global state; construct once per request scope on the server, once per
16
+ * app on the client.
17
+ */
18
+
19
+ import { StoreApiError } from "./types"
20
+
21
+ export interface StorefrontClientConfig {
22
+ /** Deployment origin, e.g. `https://admin.mindpages.bg` */
23
+ baseUrl: string
24
+ /** Tenant id — required for anon reads. */
25
+ clientId: string
26
+ /** Publishable API key (channel scope). Optional for single-channel stores. */
27
+ publishableKey?: string
28
+ /** Called per request; return the customer JWT or null for guests. */
29
+ getAuthToken?: () => string | null | Promise<string | null>
30
+ /** Called per request; return the active locale code or null. */
31
+ getLocale?: () => string | null | Promise<string | null>
32
+ /** Override fetch (tests, custom caching). Defaults to global fetch. */
33
+ fetch?: typeof fetch
34
+ }
35
+
36
+ export type QueryValue = string | number | boolean | null | undefined
37
+ export type Query = Record<string, QueryValue | QueryValue[]>
38
+
39
+ export interface RequestOptions {
40
+ method?: "GET" | "POST" | "DELETE"
41
+ query?: Query
42
+ body?: unknown
43
+ headers?: Record<string, string>
44
+ /** Passed through to fetch — lets Next.js RSC callers tag cache behavior. */
45
+ cache?: RequestCache
46
+ next?: { revalidate?: number | false; tags?: string[] }
47
+ signal?: AbortSignal
48
+ }
49
+
50
+ export class StorefrontClient {
51
+ constructor(private readonly config: StorefrontClientConfig) {
52
+ if (!config.baseUrl) throw new Error("StorefrontClient: baseUrl is required")
53
+ if (!config.clientId) throw new Error("StorefrontClient: clientId is required")
54
+ }
55
+
56
+ /** The low-level typed request. Domain modules call this — apps rarely should. */
57
+ async request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
58
+ const url = new URL(
59
+ path.replace(/^\//, ""),
60
+ this.config.baseUrl.replace(/\/?$/, "/")
61
+ )
62
+ if (opts.query) {
63
+ for (const [key, value] of Object.entries(opts.query)) {
64
+ if (value === undefined || value === null) continue
65
+ if (Array.isArray(value)) {
66
+ const joined = value.filter((v) => v !== undefined && v !== null).join(",")
67
+ if (joined) url.searchParams.set(key, joined)
68
+ } else {
69
+ url.searchParams.set(key, String(value))
70
+ }
71
+ }
72
+ }
73
+
74
+ const headers: Record<string, string> = {
75
+ "x-client-id": this.config.clientId,
76
+ ...(this.config.publishableKey
77
+ ? { "x-publishable-api-key": this.config.publishableKey }
78
+ : {}),
79
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
80
+ ...(opts.headers ?? {}),
81
+ }
82
+ const token = this.config.getAuthToken ? await this.config.getAuthToken() : null
83
+ if (token && !headers.authorization) headers.authorization = `Bearer ${token}`
84
+ const locale = this.config.getLocale ? await this.config.getLocale() : null
85
+ if (locale && !headers["x-locale"]) headers["x-locale"] = locale
86
+
87
+ const doFetch = this.config.fetch ?? fetch
88
+ const res = await doFetch(url.toString(), {
89
+ method: opts.method ?? "GET",
90
+ headers,
91
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
92
+ cache: opts.cache,
93
+ ...(opts.next ? { next: opts.next } : {}),
94
+ signal: opts.signal,
95
+ } as RequestInit)
96
+
97
+ if (!res.ok) {
98
+ let detail: unknown
99
+ const text = await res.text()
100
+ try {
101
+ detail = JSON.parse(text)
102
+ } catch {
103
+ detail = text
104
+ }
105
+ throw new StoreApiError(res.status, detail)
106
+ }
107
+ if (res.status === 204) return undefined as T
108
+ return (await res.json()) as T
109
+ }
110
+
111
+ get<T>(path: string, opts: Omit<RequestOptions, "method" | "body"> = {}): Promise<T> {
112
+ return this.request<T>(path, { ...opts, method: "GET" })
113
+ }
114
+
115
+ post<T>(path: string, body?: unknown, opts: Omit<RequestOptions, "method" | "body"> = {}): Promise<T> {
116
+ return this.request<T>(path, { ...opts, method: "POST", body })
117
+ }
118
+
119
+ delete<T>(path: string, body?: unknown, opts: Omit<RequestOptions, "method" | "body"> = {}): Promise<T> {
120
+ return this.request<T>(path, { ...opts, method: "DELETE", body })
121
+ }
122
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @cartbase/storefront/api — barrel.
3
+ *
4
+ * PRE-DECLARED SEAM: every domain module line below is written up front so
5
+ * fleet agents create their module file WITHOUT editing this barrel (no
6
+ * union-merge seams). Do not reorder; append new domains at the end.
7
+ */
8
+
9
+ export * from "./types"
10
+ export * from "./http"
11
+
12
+ export * as products from "./products"
13
+ export * as collections from "./collections"
14
+ export * as categories from "./categories"
15
+ export * as regions from "./regions"
16
+ export * as carts from "./carts"
17
+ export * as giftCards from "./gift-cards"
18
+ export * as checkout from "./checkout"
19
+ export * as orders from "./orders"
20
+ export * as customers from "./customers"
21
+ export * as auth from "./auth"
22
+ export * as content from "./content"
23
+ export * as menus from "./menus"
24
+ export * as metaobjects from "./metaobjects"
25
+ export * as reviews from "./reviews"
26
+ export * as search from "./search"
27
+ export * as integrations from "./integrations"
28
+ export * as consent from "./consent"
29
+ export * as redirects from "./redirects"