@wocart/storefront 1.20.0 → 1.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/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ follows [semantic versioning](https://semver.org/): breaking changes ship only
5
5
  in a new major version, and an export is marked `@deprecated` for at least one
6
6
  minor release before a major removes it.
7
7
 
8
+ ## 1.21.0 — 2026-10-24
9
+
10
+ ### Added
11
+
12
+ - **A shopper's track-order page, with no script.** `lookupOrder(input,
13
+ request, client)` and `orderLookupInput` in `@wocart/storefront/astro`, for
14
+ an Astro Action a plain form posts: it answers `found` (the order),
15
+ `invalid-number` (not an order number — the server is not asked),
16
+ `invalid-phone`, `not-found` (a wrong number or phone, never saying which),
17
+ `rate-limited` (with the wait) or `failed`, and forwards the shopper's
18
+ address so the server's limit counts per shopper. The `orderStatus` route
19
+ takes the same input.
20
+ - **The track-order page is a platform page.** `STOREFRONT_PAGE_PATHS.trackOrder`
21
+ is `/track-order` — `?order=<number>` fills its number in — so the platform
22
+ can link a shopper to it like the thank-you page, and every theme renders it
23
+ at the same place. `showsWidgetOn` treats it as a store page.
24
+ - `SHOPPER_ORDER_JOURNEY`: the stages from `placed` to `delivered`, in order,
25
+ for a tracking page's progress; `returned` and `cancelled` end an order off
26
+ it.
27
+
28
+ ### Fixed
29
+
30
+ - `notFound(Astro)` type-checks against Astro's own global and endpoint
31
+ context: `NotFoundContext.response.status` is optional, as Astro's
32
+ `ResponseInit` declares it. Nothing changes at runtime.
33
+
8
34
  ## 1.20.0 — 2026-10-24
9
35
 
10
36
  What every theme used to write itself is now the package's: the order form's
package/USAGE.md CHANGED
@@ -111,6 +111,7 @@ break those links.
111
111
  | `landing` | `/lp/:slug` | a landing page | required when the store runs landing pages |
112
112
  | `checkout` | `/checkout` | the cart's checkout | required when the store turns the cart on |
113
113
  | `thankYou` | `/thank-you` | the order confirmation | required |
114
+ | `trackOrder` | `/track-order` | where a shopper follows an order by its number and phone; `?order=<number>` fills the number in | required |
114
115
 
115
116
  How a theme implements them, in Astro's own terms:
116
117
 
@@ -1410,7 +1411,9 @@ number and the phone it was placed with prove it is theirs, as for a review.
1410
1411
  const found = await store.orders.status({ orderNumber, phone }, forwardedHeaders);
1411
1412
  if (found.success) {
1412
1413
  // found.data.stage: placed, confirmed, preparing, shipped, out_for_delivery,
1413
- // delivered, returned (refused at the door) or cancelled — SHOPPER_ORDER_STAGES
1414
+ // delivered, returned (refused at the door) or cancelled — SHOPPER_ORDER_STAGES;
1415
+ // a progress bar shows SHOPPER_ORDER_JOURNEY, placed to delivered, which
1416
+ // returned and cancelled leave
1414
1417
  // found.data.lines, found.data.total, found.data.placedAt
1415
1418
  // found.data.tracking: { number, carrier } once the carrier has the parcel
1416
1419
  }
@@ -1419,6 +1422,34 @@ if (found.success) {
1419
1422
  ~~~
1420
1423
 
1421
1424
  It never answers the address, the notes or the merchant's own statuses.
1425
+
1426
+ A theme's track-order page needs no script: an Astro Action posted by a plain
1427
+ form, with the outcome already worded in the few cases a page shows.
1428
+
1429
+ ~~~ts
1430
+ // src/actions/index.ts
1431
+ import { defineAction } from "astro:actions";
1432
+ import { lookupOrder, orderLookupInput } from "@wocart/storefront/astro";
1433
+
1434
+ export const server = {
1435
+ orderStatus: defineAction({
1436
+ accept: "form",
1437
+ input: orderLookupInput,
1438
+ handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
1439
+ }),
1440
+ };
1441
+ ~~~
1442
+
1443
+ ~~~astro
1444
+ ---
1445
+ // src/pages/track.astro — the page renders from the same POST
1446
+ const lookup = Astro.getActionResult(actions.orderStatus)?.data;
1447
+ // lookup?.status: "found" (lookup.order), "invalid-number" (the server is not
1448
+ // asked), "invalid-phone", "not-found", "rate-limited" (retryAfterSeconds) or "failed"
1449
+ ---
1450
+ <form method="POST" action={actions.orderStatus}>…</form>
1451
+ ~~~
1452
+
1422
1453
  From the browser, use the storefront routes' `orderStatus`.
1423
1454
 
1424
1455
  ### Meta browser primitives
package/dist/astro.d.ts CHANGED
@@ -6,5 +6,7 @@ export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./
6
6
  export type { PlaceOrderInput, PlaceOrderResult } from "./actions.js";
7
7
  export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
8
8
  export type { PrgDecision, ActionMiddlewareDeps, ActionDescriptor } from "./middleware.js";
9
+ export { lookupOrder, orderLookupInput } from "./order-lookup.js";
10
+ export type { OrderLookup, OrderLookupInput } from "./order-lookup.js";
9
11
  export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
10
12
  export type { NotFoundContext, ResolvedStore, StoreRequestOptions, StoreRequestResolution } from "./store-request.js";
package/dist/astro.js CHANGED
@@ -2,4 +2,5 @@ export { createProxyEndpoint } from "./endpoints/proxy.js";
2
2
  export { createStorefrontRoutes, REVIEWS_PAGE_DEFAULT, REVIEWS_PAGE_MAX, } from "./endpoints/storefront-routes.js";
3
3
  export { placeOrderInput, executePlaceOrder, collectForwardedHeaders, } from "./actions.js";
4
4
  export { runFormAction, resolveActionRedirectDecision } from "./middleware.js";
5
+ export { lookupOrder, orderLookupInput } from "./order-lookup.js";
5
6
  export { notFound, resolvePreviewStore, resolveStoreRequest } from "./store-request.js";
package/dist/catalog.d.ts CHANGED
@@ -10,6 +10,12 @@ export declare const PRODUCT_SORTS: readonly ["newest", "price-ascending", "pric
10
10
  * words.
11
11
  */
12
12
  export declare const SHOPPER_ORDER_STAGES: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered", "returned", "cancelled"];
13
+ /**
14
+ * The steps an order passes on its way to the shopper, in order — what a
15
+ * tracking page's progress shows. `returned` and `cancelled` end an order off
16
+ * this path, and a stage a newer server adds is on none of it.
17
+ */
18
+ export declare const SHOPPER_ORDER_JOURNEY: readonly ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
13
19
  /**
14
20
  * The collections every store starts with, the likeliest to hold products
15
21
  * first: what to show when a page needs a collection and none was named (an
package/dist/catalog.js CHANGED
@@ -19,6 +19,12 @@ export const SHOPPER_ORDER_STAGES = [
19
19
  "returned",
20
20
  "cancelled",
21
21
  ];
22
+ /**
23
+ * The steps an order passes on its way to the shopper, in order — what a
24
+ * tracking page's progress shows. `returned` and `cancelled` end an order off
25
+ * this path, and a stage a newer server adds is on none of it.
26
+ */
27
+ export const SHOPPER_ORDER_JOURNEY = ["placed", "confirmed", "preparing", "shipped", "out_for_delivery", "delivered"];
22
28
  /**
23
29
  * The collections every store starts with, the likeliest to hold products
24
30
  * first: what to show when a page needs a collection and none was named (an
@@ -24,6 +24,7 @@
24
24
  import { z } from "zod";
25
25
  import { AbandonedCheckoutInputSchema, BasketLineInputSchema, DeliveryDestinationSchema, ProductQuoteInputSchema, WilayaIdSchema, } from "../domain.js";
26
26
  import { MAX_BASKET_LINES } from "../basket-line.js";
27
+ import { orderLookupInput } from "../order-lookup.js";
27
28
  import { createProxyEndpoint, } from "./proxy.js";
28
29
  export const REVIEWS_PAGE_DEFAULT = 20;
29
30
  export const REVIEWS_PAGE_MAX = 50;
@@ -82,10 +83,6 @@ const reviewSubmitInput = z.object({
82
83
  grant: z.string().min(1).max(2048).optional(),
83
84
  uploads: z.array(z.string().min(1).max(64)).max(8).optional(),
84
85
  });
85
- const orderStatusInput = z.object({
86
- orderNumber: z.string().min(1).max(64),
87
- phone: z.string().min(1).max(32),
88
- });
89
86
  const reviewEligibilityInput = z.object({
90
87
  orderNumber: z.string().min(1).max(64),
91
88
  phone: z.string().min(1).max(32),
@@ -197,7 +194,7 @@ export function createStorefrontRoutes(options) {
197
194
  }),
198
195
  orderStatus: route({
199
196
  method: "POST",
200
- inputSchema: orderStatusInput,
197
+ inputSchema: orderLookupInput,
201
198
  forwardAttribution: true,
202
199
  call: (client, proof, headers) => client.orders.status(proof, headers),
203
200
  }),
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-re
34
34
  export { createQuoter } from "./quote.js";
35
35
  export type { Quoter, QuoterOptions, QuoteResult } from "./quote.js";
36
36
  export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
37
- export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
37
+ export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
38
38
  export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
39
39
  export type { PageMeta, ProductStructuredDataOptions } from "./seo.js";
40
40
  export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export { isWilayaId } from "./wilaya-id.js";
25
25
  export { readCartQuote, readCouponOutcome, readShippingRates } from "./answer-readers.js";
26
26
  export { createQuoter } from "./quote.js";
27
27
  export { isOrderNumber, ORDER_NUMBER_PATTERN } from "./order-number.js";
28
- export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_STAGES } from "./catalog.js";
28
+ export { DEFAULT_COLLECTION_HANDLES, PRODUCT_SORTS, SHOPPER_ORDER_JOURNEY, SHOPPER_ORDER_STAGES } from "./catalog.js";
29
29
  export { SEO_TEXT_LIMITS, jsonLd, productMeta, productStructuredData } from "./seo.js";
30
30
  export { REVIEW_REFUSAL_CODES, REVIEW_TEXT_LIMITS } from "./reviews.js";
31
31
  export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A shopper following their order from a theme's own page: the form's input,
3
+ * and what the lookup comes to — in the few outcomes a page words, never a
4
+ * server code to interpret.
5
+ *
6
+ * ```ts
7
+ * // src/actions/index.ts
8
+ * orderStatus: defineAction({
9
+ * accept: "form",
10
+ * input: orderLookupInput,
11
+ * handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
12
+ * }),
13
+ * ```
14
+ */
15
+ import { z } from "zod";
16
+ import type { StoreClient } from "./client/index.js";
17
+ import type { ShopperOrderStatus } from "./types.js";
18
+ /** The order number and the phone it was placed with, as a form posts them. */
19
+ export declare const orderLookupInput: z.ZodObject<{
20
+ orderNumber: z.ZodString;
21
+ phone: z.ZodString;
22
+ }, z.core.$strip>;
23
+ export type OrderLookupInput = z.infer<typeof orderLookupInput>;
24
+ /**
25
+ * - `found` — the order, as a shopper may see it.
26
+ * - `invalid-number` — what was typed is not an order number; the server is not asked.
27
+ * - `invalid-phone` — the server cannot read the phone as a phone number.
28
+ * - `not-found` — no order has that number and phone; which of the two is wrong is never said.
29
+ * - `rate-limited` — too many tries from this shopper or for this order; try again after the wait.
30
+ * - `failed` — nothing the shopper can fix: try again later.
31
+ */
32
+ export type OrderLookup = {
33
+ status: "found";
34
+ order: ShopperOrderStatus;
35
+ } | {
36
+ status: "invalid-number";
37
+ } | {
38
+ status: "invalid-phone";
39
+ } | {
40
+ status: "not-found";
41
+ } | {
42
+ status: "rate-limited";
43
+ retryAfterSeconds: number | null;
44
+ } | {
45
+ status: "failed";
46
+ };
47
+ /**
48
+ * Look an order up for its shopper, forwarding their address so the server's
49
+ * limit counts per shopper rather than per theme server.
50
+ */
51
+ export declare function lookupOrder(input: OrderLookupInput, request: Request, client: StoreClient): Promise<OrderLookup>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A shopper following their order from a theme's own page: the form's input,
3
+ * and what the lookup comes to — in the few outcomes a page words, never a
4
+ * server code to interpret.
5
+ *
6
+ * ```ts
7
+ * // src/actions/index.ts
8
+ * orderStatus: defineAction({
9
+ * accept: "form",
10
+ * input: orderLookupInput,
11
+ * handler: (input, context) => lookupOrder(input, context.request, context.locals.store.client),
12
+ * }),
13
+ * ```
14
+ */
15
+ import { z } from "zod";
16
+ import { collectForwardedHeaders } from "./actions.js";
17
+ import { isOrderNumber } from "./order-number.js";
18
+ /** The order number and the phone it was placed with, as a form posts them. */
19
+ export const orderLookupInput = z.object({
20
+ orderNumber: z.string().trim().min(1).max(64),
21
+ phone: z.string().trim().min(1).max(32),
22
+ });
23
+ /**
24
+ * Look an order up for its shopper, forwarding their address so the server's
25
+ * limit counts per shopper rather than per theme server.
26
+ */
27
+ export async function lookupOrder(input, request, client) {
28
+ if (!isOrderNumber(input.orderNumber))
29
+ return { status: "invalid-number" };
30
+ const result = await client.orders.status({ orderNumber: input.orderNumber.trim(), phone: input.phone.trim() }, collectForwardedHeaders(request));
31
+ if (result.success)
32
+ return { status: "found", order: result.data };
33
+ switch (result.domainError.code) {
34
+ case "ORDER_NOT_FOUND":
35
+ return { status: "not-found" };
36
+ case "VALIDATION_FAILED":
37
+ // The number was checked against the server's own pattern: what it refuses now is the phone.
38
+ return { status: "invalid-phone" };
39
+ case "ORDER_LOOKUP_RATE_LIMITED":
40
+ return { status: "rate-limited", retryAfterSeconds: result.domainError.retryAfterSeconds ?? null };
41
+ default:
42
+ return { status: "failed" };
43
+ }
44
+ }
package/dist/routes.d.ts CHANGED
@@ -26,6 +26,8 @@ export declare const STOREFRONT_PAGE_PATHS: {
26
26
  readonly landing: "/lp/:slug";
27
27
  readonly checkout: "/checkout";
28
28
  readonly thankYou: "/thank-you";
29
+ /** Where a shopper follows an order by its number and phone; `?order=<number>` fills the number in. */
30
+ readonly trackOrder: "/track-order";
29
31
  };
30
32
  /** What a storefront URL means. */
31
33
  export type StorefrontPageType = keyof typeof STOREFRONT_PAGE_PATHS;
package/dist/routes.js CHANGED
@@ -9,6 +9,8 @@ export const STOREFRONT_PAGE_PATHS = {
9
9
  landing: "/lp/:slug",
10
10
  checkout: "/checkout",
11
11
  thankYou: "/thank-you",
12
+ /** Where a shopper follows an order by its number and phone; `?order=<number>` fills the number in. */
13
+ trackOrder: "/track-order",
12
14
  };
13
15
  /**
14
16
  * The public path of a page type, its parameter encoded:
@@ -65,6 +67,7 @@ const WIDGET_SURFACE = {
65
67
  landing: "landing",
66
68
  checkout: "checkout",
67
69
  thankYou: "thankYou",
70
+ trackOrder: "pages",
68
71
  };
69
72
  /**
70
73
  * @deprecated Use `STOREFRONT_PAGE_PATHS` (every page type, the home and the
@@ -84,7 +84,7 @@ export interface NotFoundContext {
84
84
  set(options: false): void;
85
85
  };
86
86
  response: {
87
- status: number;
87
+ status?: number | undefined;
88
88
  };
89
89
  rewrite(path: string): Promise<Response>;
90
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
5
5
  "keywords": [
6
6
  "wocart",