@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.
- package/README.md +11 -0
- package/package.json +274 -274
- package/src/api/checkout.ts +12 -0
- package/src/api/integrations.ts +8 -3
- package/src/checkout/carrier-marks.ts +10 -49
- package/src/checkout/checkout-client.tsx +1 -6
- package/src/checkout/fulfillment-option.ts +3 -1
- package/src/checkout/index.ts +1 -1
- package/src/checkout/pickup-option.ts +32 -8
- package/src/checkout/pickup-point-selector.tsx +326 -367
- package/src/checkout/pickup-points.ts +64 -0
- package/src/checkout/shipping-method-list.tsx +40 -157
- package/src/checkout/use-checkout-orchestration.ts +44 -172
package/src/api/integrations.ts
CHANGED
|
@@ -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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
42
|
-
|
|
43
|
-
):
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
package/src/checkout/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
}
|