@cartbase/storefront 0.22.0 → 0.23.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.
@@ -219,7 +219,10 @@ export interface PickupPointsResponse {
219
219
  * platform's where it does not (Econt and BoxNow publish whole catalogues,
220
220
  * held per store for ten minutes and ranked so a town's own offices beat an
221
221
  * office on a street that shares the town's name). A storefront sends the
222
- * query and gets the right answer either way.
222
+ * query and gets the right answer either way.
223
+ * `catalogue: true` instead returns ALL eligible points (q/limit ignored),
224
+ * so a picker can rank before slicing. `locale` selects Speedy's BG/EN
225
+ * display names; automated terminals are excluded from office catalogues.
223
226
  *
224
227
  * Auth: anon (the store's key).
225
228
  * Errors: 400 when `mode` is missing or the carrier does not serve it, 404
@@ -228,11 +231,13 @@ export interface PickupPointsResponse {
228
231
  export async function listPickupPoints(
229
232
  client: StorefrontClient,
230
233
  provider: string,
231
- opts: { mode: "office" | "locker"; q?: string; limit?: number }
234
+ opts: { mode: "office" | "locker"; q?: string; limit?: number; catalogue?: boolean; locale?: string }
232
235
  ): Promise<PickupPointsResponse> {
233
236
  const query = new URLSearchParams({ mode: opts.mode })
234
237
  if (opts.q) query.set("q", opts.q)
235
- if (opts.limit) query.set("limit", String(opts.limit))
238
+ if (opts.limit) query.set("limit", String(opts.limit))
239
+ if (opts.catalogue) query.set("catalogue", "true")
240
+ if (opts.locale) query.set("locale", opts.locale)
236
241
  return client.get(
237
242
  `/api/store/integrations/${encodeURIComponent(provider)}/pickup-points?${query.toString()}`
238
243
  )
@@ -1,53 +1,14 @@
1
- /**
2
- * The carrier's own mark beside its name on a checkout shipping row.
3
- *
4
- * A shopper picks a courier by recognising it, and until 2026-09-18 a store
5
- * got names only unless it hunted down logo files and passed a map of its
6
- * own (store-package card). The marks are the platform's: the same files
7
- * the admin's integrations screens wear, served from the platform the store
8
- * already talks to, so a carrier we add tomorrow arrives with its mark and
9
- * no store copies an image anywhere.
10
- *
11
- * Keyed by the carrier's fulfillment option id, which the platform's
12
- * registry owns and a shipping option carries in `data.id`. Both modes of a
13
- * carrier wear the same mark: it names the courier, not the destination.
14
- */
15
-
16
- const MARK: Record<string, { carrier: string; alt: string }> = {
17
- "econt-address": { carrier: "econt", alt: "Econt" },
18
- "econt-office": { carrier: "econt", alt: "Econt" },
19
- "speedy-address": { carrier: "speedy", alt: "Speedy" },
20
- "speedy-office": { carrier: "speedy", alt: "Speedy" },
21
- "boxnow-locker": { carrier: "boxnow", alt: "BOX NOW" },
22
- "pigeon-address": { carrier: "pigeon", alt: "Pigeon Express" },
23
- "pigeon-office": { carrier: "pigeon", alt: "Pigeon Express" },
24
- "pigeon-locker": { carrier: "pigeon", alt: "Pigeon Express" },
25
- }
1
+ import type { StoreShippingOption } from "../api/checkout"
26
2
 
27
3
  /**
28
- * The default `logoByFulfillmentOptionId` map for a checkout's shipping
29
- * list. A store that passes its own map keeps it: this is the floor, not a
30
- * ceiling.
31
- *
32
- * `origin` is the SDK client's own (`client.origin`), which in a storefront
33
- * is the shop's own address, because the store API ships no CORS headers and
34
- * every storefront reaches it through a same-origin `/api/store/*` rewrite.
35
- * So the mark is asked for under that same path, which is the one address a
36
- * storefront can always reach; the platform answers it with the file it
37
- * already serves. Pointing at the platform's own `/integrations/…` instead
38
- * is what made every mark 404, since that path is not proxied and the shop's
39
- * own domain has no such file (found on the workbench before release).
4
+ * The carrier's mark beside its checkout row. Identity and the same-origin
5
+ * mark path arrive resolved by the platform adapter; the package keeps no
6
+ * carrier list.
40
7
  */
41
- export function carrierMarks(
42
- origin: string
43
- ): Record<string, { src: string; alt: string }> {
44
- const base = origin.replace(/\/+$/, "")
45
- const marks: Record<string, { src: string; alt: string }> = {}
46
- for (const [optionId, mark] of Object.entries(MARK)) {
47
- marks[optionId] = {
48
- src: `${base}/api/store/integrations/marks/${mark.carrier}`,
49
- alt: mark.alt,
50
- }
51
- }
52
- return marks
8
+ export function carrierMark(
9
+ option: Pick<StoreShippingOption, "carrier">
10
+ ): { src: string; alt: string } | null {
11
+ const carrier = option.carrier
12
+ if (!carrier?.mark_url) return null
13
+ return { src: carrier.mark_url, alt: carrier.name }
53
14
  }
@@ -7,7 +7,6 @@ import type { Cart } from "../api/carts"
7
7
  import type { StoreShippingOption } from "../api/checkout"
8
8
  import type { StoreCustomer } from "../api/customers"
9
9
  import { CheckoutAddressForm } from "./address-form"
10
- import { carrierMarks } from "./carrier-marks"
11
10
  import { DiscountSection } from "./discount-section"
12
11
  import { MobileCheckoutBottomBar } from "./mobile-checkout-bottom-bar"
13
12
  import { MobileCheckoutTopBar } from "./mobile-checkout-top-bar"
@@ -214,11 +213,7 @@ export function CheckoutClient({
214
213
  userCity: o.formData["shipping_address.city"] ?? "",
215
214
  userAddress: o.formData["shipping_address.address_1"] ?? "",
216
215
  }}
217
- // Every carrier wears its own mark without the store lifting a
218
- // finger; a store that passes its own map keeps it.
219
- logoByFulfillmentOptionId={
220
- logoByFulfillmentOptionId ?? carrierMarks(client.origin)
221
- }
216
+ logoByFulfillmentOptionId={logoByFulfillmentOptionId}
222
217
  />
223
218
 
224
219
  <CheckoutPaymentMethodList
@@ -16,8 +16,10 @@ import type { StoreShippingOption } from "../api/checkout"
16
16
  * because a cart carried over from that platform still speaks it.
17
17
  */
18
18
  export function fulfillmentOptionId(
19
- option: Pick<StoreShippingOption, "data"> | null | undefined
19
+ option: Pick<StoreShippingOption, "data" | "carrier"> | null | undefined
20
20
  ): string | null {
21
+ const resolved = option?.carrier?.fulfillment_option?.id
22
+ if (typeof resolved === "string" && resolved) return resolved
21
23
  const data = option?.data as
22
24
  | { fulfillment_option?: unknown; id?: unknown }
23
25
  | null
@@ -44,7 +44,7 @@ export {
44
44
  type PickupPoint,
45
45
  } from "./pickup-point-selector"
46
46
  export { pickupOptionOf, type PickupOption } from "./pickup-option"
47
- export { carrierMarks } from "./carrier-marks"
47
+ export { carrierMark } from "./carrier-marks"
48
48
  export { AddressSelect } from "./address-select"
49
49
  export { CompanyDetails } from "./company-details"
50
50
  export { DiscountSection } from "./discount-section"
@@ -1,11 +1,11 @@
1
+ import type { StoreShippingOption } from "../api/checkout"
2
+
1
3
  /**
2
4
  * Does this shipping option deliver to a POINT, and to whose?
3
5
  *
4
- * Every carrier names its fulfillment options `<carrier>-<mode>`, because the
5
- * platform's registry declares them that way and the same ids key the carrier
6
- * marks and the order's own destination editor. So one reader answers for all
7
- * of them, and a carrier connected tomorrow opens its picker with no line
8
- * added anywhere.
6
+ * The platform resolves provider, mode and picker scope through the carrier
7
+ * adapter. The string form remains as rollout compatibility for older store
8
+ * API responses.
9
9
  *
10
10
  * `address` is the door: there is no point to choose, so it answers null.
11
11
  *
@@ -17,19 +17,43 @@
17
17
  */
18
18
 
19
19
  export type PickupOption = {
20
- /** `econt`, `speedy`, `boxnow`, `pigeon`. */
20
+ /** Carrier adapter id. */
21
21
  provider: string
22
22
  mode: "office" | "locker"
23
+ scope: "city" | "global"
23
24
  }
24
25
 
25
26
  export function pickupOptionOf(
26
- fulfillmentOptionId: string | null | undefined
27
+ input:
28
+ | string
29
+ | Partial<Pick<StoreShippingOption, "carrier" | "data">>
30
+ | null
31
+ | undefined
27
32
  ): PickupOption | null {
33
+ if (typeof input === "object" && input) {
34
+ const carrier = input.carrier
35
+ const mode = carrier?.fulfillment_option?.mode
36
+ if (carrier && (mode === "office" || mode === "locker")) {
37
+ return { provider: carrier.id, mode, scope: carrier.pickup_scope }
38
+ }
39
+ const data = input.data as
40
+ | { fulfillment_option?: unknown; id?: unknown }
41
+ | null
42
+ | undefined
43
+ const legacy =
44
+ typeof data?.fulfillment_option === "string"
45
+ ? data.fulfillment_option
46
+ : typeof data?.id === "string"
47
+ ? data.id
48
+ : null
49
+ return pickupOptionOf(legacy)
50
+ }
51
+ const fulfillmentOptionId = input
28
52
  if (!fulfillmentOptionId) return null
29
53
  const dash = fulfillmentOptionId.lastIndexOf("-")
30
54
  if (dash <= 0) return null
31
55
  const mode = fulfillmentOptionId.slice(dash + 1)
32
56
  if (mode !== "office" && mode !== "locker") return null
33
57
  const provider = fulfillmentOptionId.slice(0, dash)
34
- return provider ? { provider, mode } : null
58
+ return provider ? { provider, mode, scope: "global" } : null
35
59
  }