@cartbase/storefront 0.22.1 → 0.24.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.
@@ -29,12 +29,11 @@ export function loadPickupPoints(
29
29
  return promise
30
30
  }
31
31
 
32
- /** BoxNow's reference picker city-locks both lists, comparing both scripts. */
32
+ /** A city-scoped adapter's eligible points, comparing both scripts. */
33
33
  export function pickupPointsInCity(points: StorePickupPoint[], city: string): StorePickupPoint[] {
34
34
  const needle = normalizeForMatch(city.trim())
35
35
  if (!needle) return points
36
- return points.filter((point) => normalizeForMatch(point.city).includes(needle) ||
37
- (point.provider === "boxnow" && normalizeForMatch(point.address).includes(needle)))
36
+ return points.filter((point) => normalizeForMatch(point.city).includes(needle))
38
37
  }
39
38
 
40
39
  /** The reference's full catalogue → distance sort → nearest three, in that order. */
@@ -1,41 +1,33 @@
1
1
  "use client"
2
2
 
3
- import type { StorefrontClient } from "../api/http"
4
- import type { StoreShippingOption } from "../api/checkout"
5
- import type { BoxNowLocker, PigeonOffice } from "../api/integrations"
6
- import { Price } from "../lib/price"
7
- import { cn } from "../lib/utils"
8
- import { useCheckoutLabels } from "./context"
9
- import {
10
- EcontOfficeSelector,
11
- type EcontOffice,
12
- } from "./econt-office-selector"
13
- import { BoxNowLockerSelector } from "./boxnow-locker-selector"
14
- import { PigeonOfficeSelector } from "./pigeon-office-selector"
15
- import {
16
- PickupPointSelector,
17
- type PickupPoint,
3
+ import type { StorefrontClient } from "../api/http"
4
+ import type { StoreShippingOption } from "../api/checkout"
5
+ import { Price } from "../lib/price"
6
+ import { cn } from "../lib/utils"
7
+ import { useCheckoutLabels } from "./context"
8
+ import {
9
+ PickupPointSelector,
10
+ type PickupPoint,
18
11
  } from "./pickup-point-selector"
19
12
  import type { PickupPointKeys } from "../api/integrations"
20
- import { pickupOptionOf, type PickupOption } from "./pickup-option"
21
- import { fulfillmentOptionId } from "./fulfillment-option"
22
- import { ErrorMessage } from "./error-message"
13
+ import { pickupOptionOf, type PickupOption } from "./pickup-option"
14
+ import { fulfillmentOptionId } from "./fulfillment-option"
15
+ import { carrierMark } from "./carrier-marks"
16
+ import { ErrorMessage } from "./error-message"
23
17
 
24
18
  /**
25
19
  * CheckoutShippingMethodList — radio-style list of shipping options.
26
- * When the user selects an Econt-office / BoxNow-locker option, the
27
- * matching picker expands inline inside that row.
20
+ * When the user selects an adapter-declared office or locker option, the
21
+ * shared picker expands inline inside that row.
28
22
  *
29
23
  * Ported from `@1click/ui/src/checkout/shipping-method-list.tsx` (v2.3.1)
30
24
  * with the Cartbase data seam: options are `StoreShippingOption` rows from
31
25
  * `checkout.listShippingOptions(client, {cart_id})` — rule-filtered and
32
26
  * display-ordered SERVER-SIDE (checkout rules + `checkout_method_order`,
33
27
  * docs/storefront/checkout.md); this component only renders what arrives.
34
- * The BoxNow picker needs the SDK transport (its locker directory is a
35
- * store-API call), so the `boxnow` config gains a `client`.
36
- *
37
- * Detection uses the stable fulfillment-option id set by the backend
38
- * provider (`shipping_option.data.id`) — NOT the display name. Admins
28
+ * Pickup catalogues use the SDK transport through one shared picker.
29
+ * Detection uses the adapter-resolved fulfillment option returned by the
30
+ * backend, never the display name. Admins
39
31
  * rename options freely, and Bulgarian labels overlap ("До точен адрес с
40
32
  * ЕКОНТ" contains "еконт" but is address delivery, not office).
41
33
  *
@@ -52,42 +44,10 @@ type CheckoutShippingMethodListProps = {
52
44
  onSelect: (id: string) => void
53
45
  addressReady: boolean
54
46
  currencyCode: string
55
- /** Optional: Econt office state + handler for inline-expand rows */
56
- econt?: {
57
- detect?: (option: StoreShippingOption) => boolean
58
- selectedOffice: EcontOffice | null
59
- onSelectOffice: (office: EcontOffice | null) => void
60
- userCity: string
61
- userAddress: string
62
- }
63
- /** Optional: BoxNow locker state + handler for inline-expand rows */
64
- boxnow?: {
65
- detect?: (option: StoreShippingOption) => boolean
66
- /** SDK transport for the locker-directory fetch. */
67
- client: StorefrontClient
68
- selectedLocker: BoxNowLocker | null
69
- onSelectLocker: (locker: BoxNowLocker | null) => void
70
- userCity: string
71
- userAddress: string
72
- }
73
- /** Optional: Pigeon Express office state + handler for inline-expand rows */
74
- pigeon?: {
75
- detect?: (option: StoreShippingOption) => boolean
76
- /** SDK transport for the pickup-point directory (the carrier's own
77
- * catalogue is credentialed, so it is served by the platform). */
78
- client: StorefrontClient
79
- selectedOffice: PigeonOffice | null
80
- onSelectOffice: (office: PigeonOffice | null) => void
81
- userCity: string
82
- userAddress: string
83
- }
84
47
  /**
85
- * The pickup picker, for EVERY carrier (2026-09-18). One entry replaces
86
- * the three above: the option's own id says which carrier and whether it
87
- * is an office or a locker, and the platform answers for all of them
88
- * through one door, so a carrier connected tomorrow opens its picker with
89
- * nothing added here. The three above stay for stores that mount them;
90
- * where both are given, the carrier-specific one wins for its own carrier.
48
+ * The pickup picker for every carrier. The option's resolved carrier
49
+ * contract says whether it is an office or locker, and the platform
50
+ * answers through one catalogue door.
91
51
  */
92
52
  pickup?: {
93
53
  /** SDK transport — the catalogue is the store's answer, not the
@@ -103,11 +63,8 @@ type CheckoutShippingMethodListProps = {
103
63
  userAddress: string
104
64
  }
105
65
  /**
106
- * Optional per-store carrier branding. Keyed by the stable fulfillment
107
- * option id (shipping_option.data.id) "econt-office", "boxnow-locker",
108
- * etc. If a match is found, a small logo renders between the radio and
109
- * the option name. Stores that don't supply a map get the unbranded
110
- * (radio + text only) layout.
66
+ * Optional per-store carrier branding override, keyed by the stable
67
+ * fulfillment option id. Without one, the adapter's mark renders.
111
68
  */
112
69
  logoByFulfillmentOptionId?: Record<string, { src: string; alt: string }>
113
70
  /** Show the shipping options as a read-only price PREVIEW before the
@@ -116,7 +73,7 @@ type CheckoutShippingMethodListProps = {
116
73
  * non-selectable — a hint tells the shopper to enter their address to
117
74
  * choose. For flat-priced stores this lets people see the cost up front
118
75
  * (a common reason carts stall) without letting them pick a method that
119
- * needs a city (Econt office / BoxNow locker). Default `false` keeps the
76
+ * needs a city (office or locker). Default `false` keeps the
120
77
  * classic address-gated placeholder for every existing store. */
121
78
  previewWhenAddressNotReady?: boolean
122
79
  }
@@ -126,21 +83,9 @@ type CheckoutShippingMethodListProps = {
126
83
  const getFulfillmentOptionId = (option: StoreShippingOption): string | null =>
127
84
  fulfillmentOptionId(option)
128
85
 
129
- const defaultEcontDetect = (option: StoreShippingOption): boolean => {
130
- return getFulfillmentOptionId(option) === "econt-office"
131
- }
132
-
133
- const defaultBoxnowDetect = (option: StoreShippingOption): boolean => {
134
- return getFulfillmentOptionId(option) === "boxnow-locker"
135
- }
136
-
137
- const defaultPigeonDetect = (option: StoreShippingOption): boolean => {
138
- return getFulfillmentOptionId(option) === "pigeon-office"
139
- }
140
-
141
- /** Which carrier, and which kind of point, from the option's own id. */
142
- const detectPickup = (option: StoreShippingOption): PickupOption | null =>
143
- pickupOptionOf(getFulfillmentOptionId(option))
86
+ /** Which carrier, which point kind and scope, from its adapter contract. */
87
+ const detectPickup = (option: StoreShippingOption): PickupOption | null =>
88
+ pickupOptionOf(option)
144
89
 
145
90
  export function CheckoutShippingMethodList({
146
91
  shippingMethods,
@@ -152,18 +97,11 @@ export function CheckoutShippingMethodList({
152
97
  onSelect,
153
98
  addressReady,
154
99
  currencyCode,
155
- econt,
156
- boxnow,
157
- pigeon,
158
- pickup,
100
+ pickup,
159
101
  logoByFulfillmentOptionId,
160
102
  previewWhenAddressNotReady = false,
161
103
  }: CheckoutShippingMethodListProps) {
162
104
  const labels = useCheckoutLabels()
163
- const detectEcont = econt?.detect ?? defaultEcontDetect
164
- const detectBoxnow = boxnow?.detect ?? defaultBoxnowDetect
165
- const detectPigeon = pigeon?.detect ?? defaultPigeonDetect
166
-
167
105
  // Preview mode: address not filled yet, but the store wants the options
168
106
  // shown as a read-only price preview rather than a placeholder. Rows are
169
107
  // rendered but locked (no selection) — see `previewWhenAddressNotReady`.
@@ -244,26 +182,14 @@ export function CheckoutShippingMethodList({
244
182
  ? option.amount
245
183
  : calculatedPricesMap[option.id]
246
184
  const isFree = price === 0
247
- const isEcontOffice = econt && detectEcont(option)
248
- const isBoxnowLocker =
249
- boxnow && !isEcontOffice && detectBoxnow(option)
250
- const isPigeonOffice =
251
- pigeon && !isEcontOffice && !isBoxnowLocker && detectPigeon(option)
252
- // Any option that delivers to a point, whatever the carrier
253
- // (2026-09-18). The three flags above are the same question asked
254
- // per carrier, which is why Speedy had no picker; this one reads
255
- // the option's own id, so a carrier connected tomorrow opens its
256
- // picker with no line added here. It yields to a legacy picker
257
- // when the store still mounts one for that carrier.
258
- const pickupOption = pickup ? detectPickup(option) : null
259
- const isPickup =
260
- !!pickupOption && !isEcontOffice && !isBoxnowLocker && !isPigeonOffice
261
- const hasExpanded =
262
- selected &&
263
- (isEcontOffice || isBoxnowLocker || isPigeonOffice || isPickup)
264
- const logo = logoByFulfillmentOptionId
265
- ? logoByFulfillmentOptionId[getFulfillmentOptionId(option) ?? ""]
266
- : undefined
185
+ // Any option whose adapter resolves to a pickup destination.
186
+ const pickupOption = pickup ? detectPickup(option) : null
187
+ const isPickup = !!pickupOption
188
+ const hasExpanded = selected && isPickup
189
+ const logo =
190
+ logoByFulfillmentOptionId?.[getFulfillmentOptionId(option) ?? ""] ??
191
+ carrierMark(option) ??
192
+ undefined
267
193
 
268
194
  return (
269
195
  <div
@@ -312,22 +238,7 @@ export function CheckoutShippingMethodList({
312
238
  <span className="text-sm font-medium text-foreground">
313
239
  {option.name}
314
240
  </span>
315
- {selected && isEcontOffice && econt?.selectedOffice && (
316
- <p className="text-xs text-muted-foreground mt-0.5">
317
- {econt.selectedOffice.name}
318
- </p>
319
- )}
320
- {selected && isBoxnowLocker && boxnow?.selectedLocker && (
321
- <p className="text-xs text-muted-foreground mt-0.5">
322
- {boxnow.selectedLocker.title}
323
- </p>
324
- )}
325
- {selected && isPigeonOffice && pigeon?.selectedOffice && (
326
- <p className="text-xs text-muted-foreground mt-0.5">
327
- {pigeon.selectedOffice.name}
328
- </p>
329
- )}
330
- {selected && isPickup && pickup?.selectedPoint && (
241
+ {selected && isPickup && pickup?.selectedPoint && (
331
242
  <p className="text-xs text-muted-foreground mt-0.5">
332
243
  {pickup.selectedPoint.name}
333
244
  </p>
@@ -371,40 +282,12 @@ export function CheckoutShippingMethodList({
371
282
  </span>
372
283
  </button>
373
284
 
374
- {hasExpanded && isEcontOffice && econt && (
375
- <EcontOfficeSelector
376
- userCity={econt.userCity}
377
- userAddress={econt.userAddress}
378
- selectedOffice={econt.selectedOffice}
379
- onSelect={econt.onSelectOffice}
380
- />
381
- )}
382
-
383
- {hasExpanded && isBoxnowLocker && boxnow && (
384
- <BoxNowLockerSelector
385
- client={boxnow.client}
386
- userCity={boxnow.userCity}
387
- userAddress={boxnow.userAddress}
388
- selectedLocker={boxnow.selectedLocker}
389
- onSelect={boxnow.onSelectLocker}
390
- />
391
- )}
392
-
393
- {hasExpanded && isPigeonOffice && pigeon && (
394
- <PigeonOfficeSelector
395
- client={pigeon.client}
396
- userCity={pigeon.userCity}
397
- userAddress={pigeon.userAddress}
398
- selectedOffice={pigeon.selectedOffice}
399
- onSelect={pigeon.onSelectOffice}
400
- />
401
- )}
402
-
403
- {hasExpanded && isPickup && pickup && pickupOption && (
285
+ {hasExpanded && isPickup && pickup && pickupOption && (
404
286
  <PickupPointSelector
405
287
  client={pickup.client}
406
- provider={pickupOption.provider}
407
- mode={pickupOption.mode}
288
+ provider={pickupOption.provider}
289
+ mode={pickupOption.mode}
290
+ scope={pickupOption.scope}
408
291
  userCity={pickup.userCity}
409
292
  userAddress={pickup.userAddress}
410
293
  selectedPoint={pickup.selectedPoint}
@@ -49,11 +49,9 @@ import { clearCartCookie } from "../lib/cookie-names"
49
49
  import { useCartDrawer } from "../cart-drawer/context"
50
50
  import compareAddresses from "./compare-addresses"
51
51
  import { translateAddressError } from "./address-error-copy"
52
- import { translatePaymentError } from "./payment-error-copy"
53
- import { useCheckoutLabels, useOrderConfirmedPath } from "./context"
54
- import type { EcontOffice } from "./econt-office-selector"
55
- import type { BoxNowLocker, PigeonOffice } from "../api/integrations"
56
- import { fulfillmentOptionId } from "./fulfillment-option"
52
+ import { translatePaymentError } from "./payment-error-copy"
53
+ import { useCheckoutLabels, useOrderConfirmedPath } from "./context"
54
+ import { fulfillmentOptionId } from "./fulfillment-option"
57
55
  import { useCheckoutFunnel } from "./use-checkout-funnel"
58
56
  import { amountDueAfterTender, isNothingToPay } from "./summary-math"
59
57
  import { forgetFiredEvents } from "../tracking/once"
@@ -931,72 +929,7 @@ export function useCheckoutOrchestration({
931
929
  // Server-side, prepare-checkout removes the PREVIOUS prepare's carrier
932
930
  // keys before merging (the `_prepared_carrier_keys` marker), so
933
931
  // switching carriers can never leak stale fields into the order.
934
- const [selectedEcontOffice, setSelectedEcontOffice] =
935
- useState<EcontOffice | null>(
936
- cart?.metadata?.econt_office_code
937
- ? ({
938
- code: cart.metadata.econt_office_code as string,
939
- name: cart.metadata.econt_office_name as string,
940
- } as EcontOffice)
941
- : null
942
- )
943
-
944
- const handleSelectEcontOffice = useCallback(
945
- (office: EcontOffice | null) => {
946
- setSelectedEcontOffice(office)
947
- },
948
- []
949
- )
950
-
951
- const [selectedBoxnowLocker, setSelectedBoxnowLocker] =
952
- useState<BoxNowLocker | null>(
953
- cart?.metadata?.boxnow_locker_id
954
- ? ({
955
- id: cart.metadata.boxnow_locker_id as string,
956
- title: (cart.metadata.boxnow_locker_title as string) ?? "",
957
- addressLine1:
958
- (cart.metadata.boxnow_locker_address as string) ?? "",
959
- addressLine2: "",
960
- postalCode: (cart.metadata.boxnow_locker_postal as string) ?? "",
961
- country: "",
962
- lat: null,
963
- lng: null,
964
- note: "",
965
- } as BoxNowLocker)
966
- : null
967
- )
968
-
969
- const handleSelectBoxnowLocker = useCallback(
970
- (locker: BoxNowLocker | null) => {
971
- setSelectedBoxnowLocker(locker)
972
- },
973
- []
974
- )
975
-
976
- // Pigeon Express keeps offices and lockers in ONE catalogue, told apart by
977
- // which id key the order carries (`pigeon_office_id` here; the platform's
978
- // destination registry owns both names).
979
- const [selectedPigeonOffice, setSelectedPigeonOffice] =
980
- useState<PigeonOffice | null>(
981
- cart?.metadata?.pigeon_office_id
982
- ? ({
983
- id: cart.metadata.pigeon_office_id as string,
984
- name: (cart.metadata.pigeon_point_name as string) ?? "",
985
- type: "office",
986
- city: (cart.metadata.pigeon_point_city as string) ?? "",
987
- address: (cart.metadata.pigeon_point_address as string) ?? "",
988
- } as PigeonOffice)
989
- : null
990
- )
991
-
992
- const handleSelectPigeonOffice = useCallback(
993
- (office: PigeonOffice | null) => {
994
- setSelectedPigeonOffice(office)
995
- },
996
- []
997
- )
998
-
999
- /**
932
+ /**
1000
933
  * THE pickup point, whatever the carrier (2026-09-18).
1001
934
  *
1002
935
  * The three states above are one state per carrier, written into one
@@ -1044,9 +977,37 @@ export function useCheckoutOrchestration({
1044
977
  // The offline tab = a merchant METHOD (the COD switch, else the first
1045
978
  // manual method) — sessions initiate by payment_method_id, provider NULL.
1046
979
  const offlineMethod = findOfflineMethod(effectiveAvailablePaymentMethods)
1047
- const hasCod = !!offlineMethod
1048
- const codMethodId = offlineMethod?.payment_method_id
1049
- const offlineIsCodKind = offlineMethod?.kind === "cod"
980
+ const hasCod = !!offlineMethod
981
+ const codMethodId = offlineMethod?.payment_method_id
982
+ const offlineIsCodKind = offlineMethod?.kind === "cod"
983
+
984
+ // Preserve the platform listing order all the way into the checkout UI.
985
+ // The API has already applied checkout_method_order; collapsing the rows
986
+ // into card/COD booleans used to throw that order away.
987
+ const paymentMethodOrder = useMemo(() => {
988
+ const order: Array<"card" | "cod"> = []
989
+ for (const entry of effectiveAvailablePaymentMethods ?? []) {
990
+ if (
991
+ hasCard &&
992
+ "id" in entry &&
993
+ entry.id === cardEntry?.id &&
994
+ !order.includes("card")
995
+ ) {
996
+ order.push("card")
997
+ }
998
+ if (
999
+ hasCod &&
1000
+ "payment_method_id" in entry &&
1001
+ entry.payment_method_id === codMethodId &&
1002
+ !order.includes("cod")
1003
+ ) {
1004
+ order.push("cod")
1005
+ }
1006
+ }
1007
+ if (hasCard && !order.includes("card")) order.push("card")
1008
+ if (hasCod && !order.includes("cod")) order.push("cod")
1009
+ return order
1010
+ }, [effectiveAvailablePaymentMethods, hasCard, cardEntry?.id, hasCod, codMethodId])
1050
1011
 
1051
1012
  // The store offers a card it cannot mount: reported once, to the merchant,
1052
1013
  // because the shopper must never be the one who discovers it. Pre-2026-09-18
@@ -1063,12 +1024,12 @@ export function useCheckoutOrchestration({
1063
1024
  )
1064
1025
  }, [cardEntry, cardOffer.mountable, logError])
1065
1026
 
1066
- // Default tab: card when available, else COD. The eager-session model
1067
- // used to seed from the cart's pending session provider; in the
1068
- // deferred-intent model there is no session at mount.
1069
- const [paymentTab, setPaymentTab] = useState<"card" | "cod">(
1070
- hasCard ? "card" : "cod"
1071
- )
1027
+ // Default tab: the first method in the merchant's checkout order. The eager-session model
1028
+ // used to seed from the cart's pending session provider; in the
1029
+ // deferred-intent model there is no session at mount.
1030
+ const [paymentTab, setPaymentTab] = useState<"card" | "cod">(
1031
+ paymentMethodOrder[0] ?? (hasCard ? "card" : "cod")
1032
+ )
1072
1033
 
1073
1034
  // Optimistic method-fee state. Painted instantly on tab toggle so the
1074
1035
  // totals row shows the predicted fee BEFORE the server applies it (at
@@ -1172,9 +1133,7 @@ export function useCheckoutOrchestration({
1172
1133
  // specific destination (e.g. picking direct address after BoxNow
1173
1134
  // locker). The carrier's own keys are never eagerly written, so only
1174
1135
  // our own note is cleared beside the state.
1175
- setSelectedBoxnowLocker(null)
1176
- setSelectedEcontOffice(null)
1177
- setSelectedPickupPoint(null)
1136
+ setSelectedPickupPoint(null)
1178
1137
  setPickupKeys(null)
1179
1138
  rememberChoice({
1180
1139
  _checkout_shipping_option: id,
@@ -1207,9 +1166,6 @@ export function useCheckoutOrchestration({
1207
1166
  const selectedFulfillmentOptionId = useMemo(() => {
1208
1167
  return fulfillmentOptionId(selectedShippingOption)
1209
1168
  }, [selectedShippingOption])
1210
- const selectedIsBoxnow = selectedFulfillmentOptionId === "boxnow-locker"
1211
- const selectedIsEcont = selectedFulfillmentOptionId === "econt-office"
1212
- const selectedIsPigeon = selectedFulfillmentOptionId === "pigeon-office"
1213
1169
 
1214
1170
  /**
1215
1171
  * WHICH carrier and WHICH kind of point this option delivers to, read from
@@ -1218,38 +1174,28 @@ export function useCheckoutOrchestration({
1218
1174
  * them that way, so a carrier connected tomorrow is understood here without
1219
1175
  * a line being added. `address` is the door and has no point to pick.
1220
1176
  */
1221
- const selectedPickup = useMemo(
1222
- () => pickupOptionOf(selectedFulfillmentOptionId),
1223
- [selectedFulfillmentOptionId]
1224
- )
1225
-
1226
- // Defensive: trust cart.metadata for locker/office IDs in addition to
1227
- // local React state. On mobile the BoxNow locker selector was seen
1228
- // firing its onSelect handler in a way that updated cart.metadata
1229
- // cleanly but left the local `selectedBoxnowLocker` stale (touch event
1230
- // timing / hydration race). Without this fallback, the local-null kept
1177
+ const selectedPickup = useMemo(
1178
+ () => pickupOptionOf(selectedShippingOption),
1179
+ [selectedShippingOption]
1180
+ )
1181
+
1182
+ // Defensive: trust cart.metadata for locker/office IDs in addition to
1183
+ // local React state. A mobile picker can update cart.metadata while its
1184
+ // local point remains stale after a touch/hydration race. Without this
1185
+ // fallback, the local-null kept
1231
1186
  // `deliveryReady` false and the entire payment section turned into a
1232
1187
  // ghost — even though the cart server-side knew the locker was set.
1233
- const hasBoxnowLockerInCart = !!cart?.metadata?.boxnow_locker_id
1234
- const hasEcontOfficeInCart = !!cart?.metadata?.econt_office_code
1235
- const hasPigeonOfficeInCart = !!cart?.metadata?.pigeon_office_id
1236
-
1237
- // An option that delivers to a point is not ready until the point is
1238
- // named, whatever the carrier. The three lines under it are the same rule
1239
- // written per carrier, kept for stores still mounting the old components;
1240
- // a store on the shared picker is answered by the first.
1188
+
1189
+ // An option that delivers to a point is not ready until the point is
1190
+ // named, whatever the carrier.
1241
1191
  const pickupSatisfied =
1242
1192
  !selectedPickup ||
1243
1193
  (!!selectedPickupPoint && selectedPickupPoint.provider === selectedPickup.provider)
1244
1194
 
1245
- const deliveryReady =
1246
- (!!selectedShippingMethod ||
1247
- (cart?.shipping_methods?.length ?? 0) > 0) &&
1248
- (pickupSatisfied ||
1249
- // The legacy pickers answer for their own carriers.
1250
- (selectedIsBoxnow && (!!selectedBoxnowLocker || hasBoxnowLockerInCart)) ||
1251
- (selectedIsEcont && (!!selectedEcontOffice || hasEcontOfficeInCart)) ||
1252
- (selectedIsPigeon && (!!selectedPigeonOffice || hasPigeonOfficeInCart)))
1195
+ const deliveryReady =
1196
+ (!!selectedShippingMethod ||
1197
+ (cart?.shipping_methods?.length ?? 0) > 0) &&
1198
+ pickupSatisfied
1253
1199
 
1254
1200
  // Reconcile paymentTab with currently-available methods. When the
1255
1201
  // store's paymentMethodFilter strips a method in response to a shipping
@@ -1511,39 +1457,7 @@ export function useCheckoutOrchestration({
1511
1457
  // state. Called by PaymentButton on click.
1512
1458
  const buildPrepareCheckoutPayload =
1513
1459
  useCallback((): PrepareCheckoutInput => {
1514
- const carrierMetadata: Record<string, unknown> = {}
1515
- if (selectedEcontOffice) {
1516
- const addr = [
1517
- selectedEcontOffice.address?.street,
1518
- selectedEcontOffice.address?.num,
1519
- ]
1520
- .filter(Boolean)
1521
- .join(" ")
1522
- carrierMetadata.econt_office_code = selectedEcontOffice.code
1523
- carrierMetadata.econt_office_name = selectedEcontOffice.name
1524
- carrierMetadata.econt_office_city =
1525
- selectedEcontOffice.address?.city?.name || ""
1526
- carrierMetadata.econt_office_address = addr
1527
- carrierMetadata.econt_office_phone =
1528
- selectedEcontOffice.phones?.[0] || ""
1529
- }
1530
- if (selectedBoxnowLocker) {
1531
- carrierMetadata.boxnow_locker_id = selectedBoxnowLocker.id
1532
- carrierMetadata.boxnow_locker_title = selectedBoxnowLocker.title
1533
- carrierMetadata.boxnow_locker_address =
1534
- selectedBoxnowLocker.addressLine1 ?? ""
1535
- carrierMetadata.boxnow_locker_postal =
1536
- selectedBoxnowLocker.postalCode ?? ""
1537
- }
1538
- if (selectedPigeonOffice) {
1539
- // The platform's destination registry owns these names: the id key
1540
- // says office or locker, and the three label keys are what the order
1541
- // page and the waybill read back (src/lib/shipping/destinations.ts).
1542
- carrierMetadata.pigeon_office_id = String(selectedPigeonOffice.id)
1543
- carrierMetadata.pigeon_point_name = selectedPigeonOffice.name
1544
- carrierMetadata.pigeon_point_city = selectedPigeonOffice.city ?? ""
1545
- carrierMetadata.pigeon_point_address = selectedPigeonOffice.address ?? ""
1546
- }
1460
+ const carrierMetadata: Record<string, unknown> = {}
1547
1461
  // The carrier-agnostic pickup point (2026-09-18). The names come from
1548
1462
  // the platform's destination registry with the point itself, so this
1549
1463
  // writes what THIS carrier's booking reads without knowing which
@@ -1606,12 +1520,11 @@ export function useCheckoutOrchestration({
1606
1520
  ...tender,
1607
1521
  }
1608
1522
  }, [
1609
- formData,
1610
- selectedShippingMethod,
1611
- selectedEcontOffice,
1612
- selectedBoxnowLocker,
1613
- selectedPigeonOffice,
1614
- paymentTab,
1523
+ formData,
1524
+ selectedShippingMethod,
1525
+ selectedPickupPoint,
1526
+ pickupKeys,
1527
+ paymentTab,
1615
1528
  cardId,
1616
1529
  codMethodId,
1617
1530
  nothingToPay,
@@ -1658,18 +1571,16 @@ export function useCheckoutOrchestration({
1658
1571
  const stateSnapshot = {
1659
1572
  cart_id: cart.id,
1660
1573
  paymentTab,
1661
- cardId,
1662
- codMethodId,
1663
- selectedShippingMethod,
1664
- selectedEcontOffice: selectedEcontOffice
1665
- ? { code: selectedEcontOffice.code, name: selectedEcontOffice.name }
1666
- : null,
1667
- selectedBoxnowLocker: selectedBoxnowLocker
1668
- ? {
1669
- id: selectedBoxnowLocker.id,
1670
- title: selectedBoxnowLocker.title,
1671
- }
1672
- : null,
1574
+ cardId,
1575
+ codMethodId,
1576
+ selectedShippingMethod,
1577
+ selectedPickupPoint: selectedPickupPoint
1578
+ ? {
1579
+ provider: selectedPickupPoint.provider,
1580
+ id: selectedPickupPoint.id,
1581
+ name: selectedPickupPoint.name,
1582
+ }
1583
+ : null,
1673
1584
  hasStripeBundle: !!stripeBundle,
1674
1585
  }
1675
1586
  dbg("[buy-click] STATE", stateSnapshot)
@@ -1853,11 +1764,9 @@ export function useCheckoutOrchestration({
1853
1764
  cart.id,
1854
1765
  paymentTab,
1855
1766
  cardId,
1856
- codMethodId,
1857
- selectedShippingMethod,
1858
- selectedEcontOffice,
1859
- selectedBoxnowLocker,
1860
- selectedPigeonOffice,
1767
+ codMethodId,
1768
+ selectedShippingMethod,
1769
+ selectedPickupPoint,
1861
1770
  flushAddressSave,
1862
1771
  buildPrepareCheckoutPayload,
1863
1772
  placeOrder,
@@ -1891,12 +1800,9 @@ export function useCheckoutOrchestration({
1891
1800
  shippingMethods,
1892
1801
  calculatedPricesMap,
1893
1802
  isLoadingPrices,
1894
- selectedShippingMethod,
1895
- selectedShippingOption,
1896
- selectedFulfillmentOptionId,
1897
- selectedIsBoxnow,
1898
- selectedIsEcont,
1899
- selectedIsPigeon,
1803
+ selectedShippingMethod,
1804
+ selectedShippingOption,
1805
+ selectedFulfillmentOptionId,
1900
1806
  shippingLoading,
1901
1807
  shippingError,
1902
1808
  optimisticShippingCost,
@@ -1904,21 +1810,16 @@ export function useCheckoutOrchestration({
1904
1810
  handleSelectShipping,
1905
1811
 
1906
1812
  // Carriers
1907
- selectedEcontOffice,
1908
- handleSelectEcontOffice,
1909
- selectedBoxnowLocker,
1910
- handleSelectBoxnowLocker,
1911
- selectedPigeonOffice,
1912
- handleSelectPigeonOffice,
1913
- /** The pickup point, whatever the carrier, and which carrier and kind of
1813
+ /** The pickup point, whatever the carrier, and which carrier and kind of
1914
1814
  * point the chosen option asks for. Null on a door delivery. */
1915
1815
  selectedPickupPoint,
1916
1816
  handleSelectPickupPoint,
1917
1817
  selectedPickup,
1918
1818
 
1919
1819
  // Payment
1920
- paymentTab,
1921
- hasCard,
1820
+ paymentTab,
1821
+ paymentMethodOrder,
1822
+ hasCard,
1922
1823
  hasCod,
1923
1824
  cardId,
1924
1825
  /** The store's own Stripe, off the card entry — what `PaymentWrapper`