@wocart/storefront 0.0.0-stage → 1.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.
- package/CHANGELOG.md +95 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +144 -2
- package/USAGE.md +1470 -0
- package/dist/abandoned-checkout.d.ts +22 -0
- package/dist/abandoned-checkout.js +89 -0
- package/dist/actions.d.ts +117 -0
- package/dist/actions.js +196 -0
- package/dist/astro.d.ts +6 -0
- package/dist/astro.js +3 -0
- package/dist/basket.d.ts +87 -0
- package/dist/basket.js +258 -0
- package/dist/client/cart.d.ts +30 -0
- package/dist/client/cart.js +26 -0
- package/dist/client/categories.d.ts +25 -0
- package/dist/client/categories.js +37 -0
- package/dist/client/config.d.ts +29 -0
- package/dist/client/config.js +41 -0
- package/dist/client/core.d.ts +88 -0
- package/dist/client/core.js +566 -0
- package/dist/client/errors.d.ts +19 -0
- package/dist/client/errors.js +31 -0
- package/dist/client/geo.d.ts +31 -0
- package/dist/client/geo.js +39 -0
- package/dist/client/index.d.ts +40 -0
- package/dist/client/index.js +81 -0
- package/dist/client/orders.d.ts +68 -0
- package/dist/client/orders.js +76 -0
- package/dist/client/page-data.d.ts +30 -0
- package/dist/client/page-data.js +36 -0
- package/dist/client/pages.d.ts +33 -0
- package/dist/client/pages.js +44 -0
- package/dist/client/paths.d.ts +8 -0
- package/dist/client/paths.js +10 -0
- package/dist/client/products.d.ts +56 -0
- package/dist/client/products.js +78 -0
- package/dist/client/shipping.d.ts +36 -0
- package/dist/client/shipping.js +47 -0
- package/dist/data/communes.d.ts +25 -0
- package/dist/data/communes.js +1690 -0
- package/dist/data/wilayas.d.ts +6 -0
- package/dist/data/wilayas.js +64 -0
- package/dist/domain.d.ts +341 -0
- package/dist/domain.js +214 -0
- package/dist/endpoints/proxy.d.ts +111 -0
- package/dist/endpoints/proxy.js +161 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +30 -0
- package/dist/locale-routing.d.ts +59 -0
- package/dist/locale-routing.js +62 -0
- package/dist/locales.d.ts +15 -0
- package/dist/locales.js +8 -0
- package/dist/meta-tracking.d.ts +34 -0
- package/dist/meta-tracking.js +72 -0
- package/dist/middleware.d.ts +100 -0
- package/dist/middleware.js +72 -0
- package/dist/offer-view.d.ts +10 -0
- package/dist/offer-view.js +13 -0
- package/dist/otp.d.ts +80 -0
- package/dist/otp.js +295 -0
- package/dist/phone.d.ts +2 -0
- package/dist/phone.js +34 -0
- package/dist/product-view.d.ts +12 -0
- package/dist/product-view.js +42 -0
- package/dist/routes.d.ts +45 -0
- package/dist/routes.js +59 -0
- package/dist/schemas.d.ts +753 -0
- package/dist/schemas.js +377 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +2 -0
- package/dist/types.d.ts +418 -0
- package/dist/types.js +1 -0
- package/dist/utils/image.d.ts +140 -0
- package/dist/utils/image.js +131 -0
- package/dist/utils/money.d.ts +39 -0
- package/dist/utils/money.js +43 -0
- package/dist/utils/product-image.d.ts +37 -0
- package/dist/utils/product-image.js +38 -0
- package/dist/whatsapp.d.ts +55 -0
- package/dist/whatsapp.js +89 -0
- package/package.json +76 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store Client Factory — the SDK's front door.
|
|
3
|
+
*
|
|
4
|
+
* One server-side call builds the whole read/write surface a theme needs, over one
|
|
5
|
+
* shared core: config, products, categories, pages, shipping, geo, cart,
|
|
6
|
+
* orders. The core is internal — consumers see only namespaces.
|
|
7
|
+
*
|
|
8
|
+
* One core means one connection story for every namespace: every read is
|
|
9
|
+
* bounded by the client's `readTimeoutMs`, each call makes its own request
|
|
10
|
+
* (none waits on another's pending one), and the opt-in caches (config,
|
|
11
|
+
* categories, communes) keep finished results per client instance.
|
|
12
|
+
* A theme server render typically builds one client per request.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createStoreClient } from "@wocart/storefront/server";
|
|
16
|
+
*
|
|
17
|
+
* const store = createStoreClient({
|
|
18
|
+
* serverUrl: env.COD_SERVER_URL,
|
|
19
|
+
* apiKey: env.STORE_API_KEY,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const [config, products] = await Promise.all([
|
|
23
|
+
* store.config.get(),
|
|
24
|
+
* store.products.list({ featured: true, limit: 8 }),
|
|
25
|
+
* ]);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import type { StoreScope } from "../domain.js";
|
|
29
|
+
export declare function createStoreClient(config: StoreScope): {
|
|
30
|
+
config: import("./config.js").ConfigNamespace;
|
|
31
|
+
products: import("./products.js").ProductsNamespace;
|
|
32
|
+
categories: import("./categories.js").CategoriesNamespace;
|
|
33
|
+
pages: import("./pages.js").PagesNamespace;
|
|
34
|
+
pageData: import("./page-data.js").PageDataNamespace;
|
|
35
|
+
shipping: import("./shipping.js").ShippingNamespace;
|
|
36
|
+
geo: import("./geo.js").GeoNamespace;
|
|
37
|
+
cart: import("./cart.js").CartNamespace;
|
|
38
|
+
orders: import("./orders.js").OrdersNamespace;
|
|
39
|
+
};
|
|
40
|
+
export type StoreClient = ReturnType<typeof createStoreClient>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store Client Factory — the SDK's front door.
|
|
3
|
+
*
|
|
4
|
+
* One server-side call builds the whole read/write surface a theme needs, over one
|
|
5
|
+
* shared core: config, products, categories, pages, shipping, geo, cart,
|
|
6
|
+
* orders. The core is internal — consumers see only namespaces.
|
|
7
|
+
*
|
|
8
|
+
* One core means one connection story for every namespace: every read is
|
|
9
|
+
* bounded by the client's `readTimeoutMs`, each call makes its own request
|
|
10
|
+
* (none waits on another's pending one), and the opt-in caches (config,
|
|
11
|
+
* categories, communes) keep finished results per client instance.
|
|
12
|
+
* A theme server render typically builds one client per request.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { createStoreClient } from "@wocart/storefront/server";
|
|
16
|
+
*
|
|
17
|
+
* const store = createStoreClient({
|
|
18
|
+
* serverUrl: env.COD_SERVER_URL,
|
|
19
|
+
* apiKey: env.STORE_API_KEY,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const [config, products] = await Promise.all([
|
|
23
|
+
* store.config.get(),
|
|
24
|
+
* store.products.list({ featured: true, limit: 8 }),
|
|
25
|
+
* ]);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
import { createClientCore } from "./core.js";
|
|
29
|
+
import { StorefrontError } from "./errors.js";
|
|
30
|
+
import { createConfigNamespace } from "./config.js";
|
|
31
|
+
import { createProductsNamespace } from "./products.js";
|
|
32
|
+
import { createCategoriesNamespace } from "./categories.js";
|
|
33
|
+
import { createPagesNamespace } from "./pages.js";
|
|
34
|
+
import { createPageDataNamespace } from "./page-data.js";
|
|
35
|
+
import { createShippingNamespace } from "./shipping.js";
|
|
36
|
+
import { createGeoNamespace } from "./geo.js";
|
|
37
|
+
import { createCartNamespace } from "./cart.js";
|
|
38
|
+
import { createOrdersNamespace } from "./orders.js";
|
|
39
|
+
export function createStoreClient(config) {
|
|
40
|
+
assertUsableScope(config);
|
|
41
|
+
const core = createClientCore(config);
|
|
42
|
+
return {
|
|
43
|
+
config: createConfigNamespace(core),
|
|
44
|
+
products: createProductsNamespace(core),
|
|
45
|
+
categories: createCategoriesNamespace(core),
|
|
46
|
+
pages: createPagesNamespace(core),
|
|
47
|
+
pageData: createPageDataNamespace(core),
|
|
48
|
+
shipping: createShippingNamespace(core),
|
|
49
|
+
geo: createGeoNamespace(core),
|
|
50
|
+
cart: createCartNamespace(core),
|
|
51
|
+
orders: createOrdersNamespace(core),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One place where an unusable configuration becomes a loud setup error.
|
|
56
|
+
*
|
|
57
|
+
* An empty `serverUrl` or `apiKey` (the shape a missing environment variable
|
|
58
|
+
* takes after a cast) would otherwise send unauthenticated requests and turn
|
|
59
|
+
* every read into an empty store — a silent failure that looks like "no
|
|
60
|
+
* products". Reads keep their never-throw contract *after* construction; this
|
|
61
|
+
* throws only at setup, where the caller can still fix the variable.
|
|
62
|
+
*/
|
|
63
|
+
function assertUsableScope(config) {
|
|
64
|
+
const required = [
|
|
65
|
+
["serverUrl", "COD_SERVER_URL", config?.serverUrl],
|
|
66
|
+
["apiKey", "STORE_API_KEY", config?.apiKey],
|
|
67
|
+
];
|
|
68
|
+
for (const [field, envName, value] of required) {
|
|
69
|
+
if (typeof value === "string" && value.trim() !== "")
|
|
70
|
+
continue;
|
|
71
|
+
throw new StorefrontError({
|
|
72
|
+
category: "authentication",
|
|
73
|
+
code: "MISSING_CONFIGURATION",
|
|
74
|
+
message: `createStoreClient: \`${field}\` is empty — set ${envName} before building ` +
|
|
75
|
+
`the client. Without it every read would return an empty store instead of failing.`,
|
|
76
|
+
retryable: false,
|
|
77
|
+
operation: "client.create",
|
|
78
|
+
sideEffect: "not_started",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders Namespace - Checkout, Tracking, Abandonment, OTP
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides five endpoints and their differing wire contracts
|
|
5
|
+
* ({data} envelopes, a top-level {success, id}, fire-and-forget PATCH,
|
|
6
|
+
* OTP context flattening) behind one small surface.
|
|
7
|
+
*
|
|
8
|
+
* Failure contracts:
|
|
9
|
+
* - create/abandoned/otp return errors as results (never throw) — the
|
|
10
|
+
* checkout form must stay on screen and render the reason.
|
|
11
|
+
* - tracking resolves to null on any failure — firing no conversion event
|
|
12
|
+
* is recoverable; reporting it to the wrong ad account is not.
|
|
13
|
+
*/
|
|
14
|
+
import { type ClientCore, type ClientResult } from "./core.js";
|
|
15
|
+
import type { AbandonedCheckoutInput } from "../domain.js";
|
|
16
|
+
import type { CreateOrderInput, CreatedOrder, OrderTracking, OtpSendResult, OtpVerifyResult } from "../types.js";
|
|
17
|
+
export interface AbandonedNamespace {
|
|
18
|
+
/**
|
|
19
|
+
* Upsert an abandoned-checkout record — fire-and-forget. `forwardedHeaders`
|
|
20
|
+
* carries the shopper's User-Agent / forwarding headers so attribution
|
|
21
|
+
* reflects the visitor, not this worker.
|
|
22
|
+
*/
|
|
23
|
+
upsert(body: AbandonedCheckoutInput, forwardedHeaders?: Record<string, string>): Promise<ClientResult<{
|
|
24
|
+
id: string;
|
|
25
|
+
}>>;
|
|
26
|
+
/**
|
|
27
|
+
* Mark an abandoned session as converted after a successful order.
|
|
28
|
+
* The server treats this as fire-and-forget (always 200).
|
|
29
|
+
*/
|
|
30
|
+
convert(sessionId: string, orderId: string, orderNumber: string): Promise<ClientResult<null>>;
|
|
31
|
+
}
|
|
32
|
+
export interface OtpNamespace {
|
|
33
|
+
/**
|
|
34
|
+
* Send a WhatsApp verification code. On "unavailable" the server returns
|
|
35
|
+
* a signed bypassToken — checkout proceeds unverified (fail-open).
|
|
36
|
+
* Rate-limit errors carry `code: "OTP_RATE_LIMITED"` and `windowSeconds`.
|
|
37
|
+
*/
|
|
38
|
+
send(phone: string): Promise<ClientResult<OtpSendResult>>;
|
|
39
|
+
/**
|
|
40
|
+
* Verify the code the customer typed. Wrong-but-retryable codes return
|
|
41
|
+
* `code`/`attemptsRemaining`/`terminal` on the error result; a success
|
|
42
|
+
* returns the signed `otpToken` that travels with the order.
|
|
43
|
+
*/
|
|
44
|
+
verify(phone: string, requestId: string, code: string): Promise<ClientResult<OtpVerifyResult>>;
|
|
45
|
+
}
|
|
46
|
+
export interface OrdersNamespace {
|
|
47
|
+
/**
|
|
48
|
+
* Submit an order. The server re-prices every line from the catalog —
|
|
49
|
+
* `pricePerUnit` arriving from the browser is display-only. Attribution
|
|
50
|
+
* headers (forwarded from the proxy) ride along here, not on the body.
|
|
51
|
+
* The body is the SDK's `CreateOrderInput`; `PlaceOrderInput` (the form
|
|
52
|
+
* whitelist) is assignable to it, so an action can pass its parsed input
|
|
53
|
+
* straight through.
|
|
54
|
+
*/
|
|
55
|
+
create(body: CreateOrderInput, forwardedHeaders?: Record<string, string>): Promise<ClientResult<CreatedOrder>>;
|
|
56
|
+
/**
|
|
57
|
+
* The order's tracking destination (which Meta pixel, which browser
|
|
58
|
+
* event). Resolves to `null` on any failure or an empty orderId —
|
|
59
|
+
* the thank-you page fires no conversion at all in that case.
|
|
60
|
+
*/
|
|
61
|
+
tracking(orderId: string): Promise<OrderTracking | null>;
|
|
62
|
+
abandoned: AbandonedNamespace;
|
|
63
|
+
otp: OtpNamespace;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create orders namespace
|
|
67
|
+
*/
|
|
68
|
+
export declare function createOrdersNamespace(core: ClientCore): OrdersNamespace;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders Namespace - Checkout, Tracking, Abandonment, OTP
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides five endpoints and their differing wire contracts
|
|
5
|
+
* ({data} envelopes, a top-level {success, id}, fire-and-forget PATCH,
|
|
6
|
+
* OTP context flattening) behind one small surface.
|
|
7
|
+
*
|
|
8
|
+
* Failure contracts:
|
|
9
|
+
* - create/abandoned/otp return errors as results (never throw) — the
|
|
10
|
+
* checkout form must stay on screen and render the reason.
|
|
11
|
+
* - tracking resolves to null on any failure — firing no conversion event
|
|
12
|
+
* is recoverable; reporting it to the wrong ad account is not.
|
|
13
|
+
*/
|
|
14
|
+
import { CreatedOrderSchema, IdResponseSchema, OrderTrackingSchema, OtpSendResultSchema, OtpVerifyResultSchema, } from "../schemas.js";
|
|
15
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
16
|
+
import { reportDegradedRead } from "./core.js";
|
|
17
|
+
/**
|
|
18
|
+
* Create orders namespace
|
|
19
|
+
*/
|
|
20
|
+
export function createOrdersNamespace(core) {
|
|
21
|
+
return {
|
|
22
|
+
async create(body, forwardedHeaders) {
|
|
23
|
+
return core.post("/store/orders", body, {
|
|
24
|
+
...(forwardedHeaders ? { headers: forwardedHeaders } : {}),
|
|
25
|
+
schema: CreatedOrderSchema,
|
|
26
|
+
operation: STOREFRONT_OPERATIONS.ordersCreate,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
async tracking(orderId) {
|
|
30
|
+
if (!orderId)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const tracking = await core.get(`/store/orders/${encodeURIComponent(orderId)}/tracking`, OrderTrackingSchema.nullable(), { operation: STOREFRONT_OPERATIONS.ordersTracking });
|
|
34
|
+
return tracking ?? null;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
reportDegradedRead(core.config, error, "orders.tracking");
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
abandoned: {
|
|
42
|
+
async upsert(body, forwardedHeaders) {
|
|
43
|
+
// The server answers `{ success, id }` — the payload sits at the
|
|
44
|
+
// top level, not inside `data`.
|
|
45
|
+
const result = await core.post("/store/abandoned", body, {
|
|
46
|
+
...(forwardedHeaders ? { headers: forwardedHeaders } : {}),
|
|
47
|
+
schema: IdResponseSchema,
|
|
48
|
+
topLevelData: true,
|
|
49
|
+
operation: STOREFRONT_OPERATIONS.ordersAbandonedUpsert,
|
|
50
|
+
});
|
|
51
|
+
if (!result.success)
|
|
52
|
+
return result;
|
|
53
|
+
return { success: true, data: { id: result.data.id } };
|
|
54
|
+
},
|
|
55
|
+
async convert(sessionId, orderId, orderNumber) {
|
|
56
|
+
const result = await core.patch(`/store/abandoned/${encodeURIComponent(sessionId)}/convert`, { orderId, orderNumber }, { operation: STOREFRONT_OPERATIONS.ordersAbandonedConvert });
|
|
57
|
+
if (!result.success)
|
|
58
|
+
return result;
|
|
59
|
+
return { success: true, data: null };
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
otp: {
|
|
63
|
+
async send(phone) {
|
|
64
|
+
// core.post flattens the server's `context` (windowSeconds) and
|
|
65
|
+
// `code` into the error result — exactly what the OTP step reads.
|
|
66
|
+
return core.post("/store/otp/send", { phone }, {
|
|
67
|
+
schema: OtpSendResultSchema,
|
|
68
|
+
operation: STOREFRONT_OPERATIONS.ordersOtpSend,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
async verify(phone, requestId, code) {
|
|
72
|
+
return core.post("/store/otp/verify", { phone, requestId, code }, { schema: OtpVerifyResultSchema, operation: STOREFRONT_OPERATIONS.ordersOtpVerify });
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Data Namespace — the store's saved composition document.
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction and transport behind one call. The
|
|
5
|
+
* document belongs to the store (one per surface — "home" today) and is
|
|
6
|
+
* written by the platform's editor; this namespace is the theme's read side.
|
|
7
|
+
*
|
|
8
|
+
* Transport semantics (NOT the document contract): the payload is validated
|
|
9
|
+
* as `{ version, surface, sections }` and returns `null` for "no saved
|
|
10
|
+
* document" or any failure — never throws. What the sections MEAN is the
|
|
11
|
+
* theme SDK's `parsePageData`'s job; `sections` stays `unknown[]` here so a
|
|
12
|
+
* newer document's instances reach the theme verbatim.
|
|
13
|
+
*
|
|
14
|
+
* Fetched fresh on every call (no process-lifetime cache — a save must reach
|
|
15
|
+
* the next render, subject only to the theme's route cache).
|
|
16
|
+
*/
|
|
17
|
+
import { type ClientCore } from "./core.js";
|
|
18
|
+
import type { PageDocumentPayload } from "../types.js";
|
|
19
|
+
export interface PageDataNamespace {
|
|
20
|
+
/**
|
|
21
|
+
* Get the store's saved page document for one surface (e.g. "home").
|
|
22
|
+
* Resolves to `null` when the store has never saved one or when the read
|
|
23
|
+
* fails — the theme then renders its shipped default document.
|
|
24
|
+
*/
|
|
25
|
+
get(surface: string): Promise<PageDocumentPayload | null>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create the page-data namespace.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createPageDataNamespace(core: ClientCore): PageDataNamespace;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Data Namespace — the store's saved composition document.
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction and transport behind one call. The
|
|
5
|
+
* document belongs to the store (one per surface — "home" today) and is
|
|
6
|
+
* written by the platform's editor; this namespace is the theme's read side.
|
|
7
|
+
*
|
|
8
|
+
* Transport semantics (NOT the document contract): the payload is validated
|
|
9
|
+
* as `{ version, surface, sections }` and returns `null` for "no saved
|
|
10
|
+
* document" or any failure — never throws. What the sections MEAN is the
|
|
11
|
+
* theme SDK's `parsePageData`'s job; `sections` stays `unknown[]` here so a
|
|
12
|
+
* newer document's instances reach the theme verbatim.
|
|
13
|
+
*
|
|
14
|
+
* Fetched fresh on every call (no process-lifetime cache — a save must reach
|
|
15
|
+
* the next render, subject only to the theme's route cache).
|
|
16
|
+
*/
|
|
17
|
+
import { PageDocumentPayloadSchema } from "../schemas.js";
|
|
18
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
19
|
+
import { reportDegradedRead } from "./core.js";
|
|
20
|
+
/**
|
|
21
|
+
* Create the page-data namespace.
|
|
22
|
+
*/
|
|
23
|
+
export function createPageDataNamespace(core) {
|
|
24
|
+
return {
|
|
25
|
+
async get(surface) {
|
|
26
|
+
try {
|
|
27
|
+
const document = await core.get(`/store/page-documents/${encodeURIComponent(surface)}`, PageDocumentPayloadSchema.nullable(), { operation: STOREFRONT_OPERATIONS.pageDataGet });
|
|
28
|
+
return document ?? null;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
reportDegradedRead(core.config, error, "pageData.get");
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages Namespace - Store Pages & Landing Pages
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction, slug encoding, and transport behind
|
|
5
|
+
* two calls. Pages are fetched fresh every request (a merchant edit is live
|
|
6
|
+
* on the next render).
|
|
7
|
+
*
|
|
8
|
+
* Failure contracts:
|
|
9
|
+
* - `get(slug)` → null means "not published" — the theme 404s, never a
|
|
10
|
+
* soft 200 with empty content.
|
|
11
|
+
* - `landing(slug)` → null means draft/archived/unknown — the theme
|
|
12
|
+
* redirects to the store home instead of a dead end.
|
|
13
|
+
*/
|
|
14
|
+
import { type ClientCore } from "./core.js";
|
|
15
|
+
import type { LandingPage, StorePagePublic } from "../types.js";
|
|
16
|
+
export interface PagesNamespace {
|
|
17
|
+
/**
|
|
18
|
+
* Get a published store page (Terms/Privacy/Refund/Shipping/custom),
|
|
19
|
+
* resolved to the store's own language server-side.
|
|
20
|
+
* Resolves to `null` for draft/unknown slugs or fetch failure.
|
|
21
|
+
*/
|
|
22
|
+
get(slug: string): Promise<StorePagePublic | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Get a published landing page by its public slug: the ordered image
|
|
25
|
+
* stack, spacing settings, and the linked product in full store-product
|
|
26
|
+
* shape. Resolves to `null` for draft/archived/unknown slugs or failure.
|
|
27
|
+
*/
|
|
28
|
+
landing(slug: string): Promise<LandingPage | null>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create pages namespace
|
|
32
|
+
*/
|
|
33
|
+
export declare function createPagesNamespace(core: ClientCore): PagesNamespace;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages Namespace - Store Pages & Landing Pages
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction, slug encoding, and transport behind
|
|
5
|
+
* two calls. Pages are fetched fresh every request (a merchant edit is live
|
|
6
|
+
* on the next render).
|
|
7
|
+
*
|
|
8
|
+
* Failure contracts:
|
|
9
|
+
* - `get(slug)` → null means "not published" — the theme 404s, never a
|
|
10
|
+
* soft 200 with empty content.
|
|
11
|
+
* - `landing(slug)` → null means draft/archived/unknown — the theme
|
|
12
|
+
* redirects to the store home instead of a dead end.
|
|
13
|
+
*/
|
|
14
|
+
import { LandingPageSchema, StorePagePublicSchema } from "../schemas.js";
|
|
15
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
16
|
+
import { localizedPath } from "./paths.js";
|
|
17
|
+
import { reportDegradedRead } from "./core.js";
|
|
18
|
+
/**
|
|
19
|
+
* Create pages namespace
|
|
20
|
+
*/
|
|
21
|
+
export function createPagesNamespace(core) {
|
|
22
|
+
return {
|
|
23
|
+
async get(slug) {
|
|
24
|
+
try {
|
|
25
|
+
const page = await core.get(localizedPath(core.config, `/store/pages/${encodeURIComponent(slug)}`), StorePagePublicSchema.nullable(), { operation: STOREFRONT_OPERATIONS.pagesGet });
|
|
26
|
+
return page ?? null;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
reportDegradedRead(core.config, error, "pages.get");
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async landing(slug) {
|
|
34
|
+
try {
|
|
35
|
+
const landingPage = await core.get(`/store/landing-pages/${encodeURIComponent(slug)}`, LandingPageSchema.nullable(), { operation: STOREFRONT_OPERATIONS.pagesLanding });
|
|
36
|
+
return landingPage ?? null;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
reportDegradedRead(core.config, error, "pages.landing");
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request paths for reads that carry merchant text. Each one names the
|
|
3
|
+
* client's language (`StoreScope.locale`), so the server serves it
|
|
4
|
+
* with its translations — and caches keyed by path keep one entry per
|
|
5
|
+
* language.
|
|
6
|
+
*/
|
|
7
|
+
import type { StoreScope } from "../domain.js";
|
|
8
|
+
export declare function localizedPath(scope: Pick<StoreScope, "locale">, path: string, params?: Readonly<Record<string, string | undefined>>): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function localizedPath(scope, path, params = {}) {
|
|
2
|
+
const search = new URLSearchParams();
|
|
3
|
+
for (const [name, value] of Object.entries(params))
|
|
4
|
+
if (value !== undefined)
|
|
5
|
+
search.set(name, value);
|
|
6
|
+
if (scope.locale !== undefined)
|
|
7
|
+
search.set("locale", scope.locale);
|
|
8
|
+
const query = search.toString();
|
|
9
|
+
return query === "" ? path : `${path}?${query}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products Namespace - Product Catalog & Reviews
|
|
3
|
+
*
|
|
4
|
+
* Deep module: Hides ID vs handle resolution, pagination, filter construction,
|
|
5
|
+
* response validation, and review submission complexity. Callers get a clean
|
|
6
|
+
* namespace interface.
|
|
7
|
+
*
|
|
8
|
+
* Every read validates the server's payload against the published catalog
|
|
9
|
+
* schemas before it is handed out: a catalog response that no longer matches
|
|
10
|
+
* the contract degrades to `null`/`[]` like any other failed read (and is
|
|
11
|
+
* reported through the client's `onError`), instead of leaking a
|
|
12
|
+
* half-shaped product into a theme.
|
|
13
|
+
*/
|
|
14
|
+
import { type ClientCore, type ClientResult } from "./core.js";
|
|
15
|
+
import type { Product, ProductList, ProductListFilters, Review, ReviewInput, ReviewListPagination } from "../types.js";
|
|
16
|
+
export interface ProductsNamespace {
|
|
17
|
+
/**
|
|
18
|
+
* Get a single product by its public URL handle
|
|
19
|
+
* @param handle - URL handle
|
|
20
|
+
* @returns Product or null if not found
|
|
21
|
+
*/
|
|
22
|
+
get(handle: string): Promise<Product | null>;
|
|
23
|
+
/**
|
|
24
|
+
* List products with optional filters
|
|
25
|
+
* @param filters - Optional category, featured flag, limit and offset
|
|
26
|
+
* @returns Array of products
|
|
27
|
+
*/
|
|
28
|
+
list(filters?: ProductListFilters): Promise<ProductList[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Reviews sub-namespace
|
|
31
|
+
*/
|
|
32
|
+
reviews: {
|
|
33
|
+
/**
|
|
34
|
+
* List reviews for a product
|
|
35
|
+
* @param productId - Product ID
|
|
36
|
+
* @param pagination - Optional limit and offset
|
|
37
|
+
* @returns Reviews with total count
|
|
38
|
+
*/
|
|
39
|
+
list(productId: string, pagination?: ReviewListPagination): Promise<{
|
|
40
|
+
rows: Review[];
|
|
41
|
+
total: number;
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Submit a product review
|
|
45
|
+
* @param data - Review data
|
|
46
|
+
* @returns Result with review ID or error
|
|
47
|
+
*/
|
|
48
|
+
submit(data: ReviewInput): Promise<ClientResult<{
|
|
49
|
+
id: string;
|
|
50
|
+
}>>;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create products namespace
|
|
55
|
+
*/
|
|
56
|
+
export declare function createProductsNamespace(core: ClientCore): ProductsNamespace;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products Namespace - Product Catalog & Reviews
|
|
3
|
+
*
|
|
4
|
+
* Deep module: Hides ID vs handle resolution, pagination, filter construction,
|
|
5
|
+
* response validation, and review submission complexity. Callers get a clean
|
|
6
|
+
* namespace interface.
|
|
7
|
+
*
|
|
8
|
+
* Every read validates the server's payload against the published catalog
|
|
9
|
+
* schemas before it is handed out: a catalog response that no longer matches
|
|
10
|
+
* the contract degrades to `null`/`[]` like any other failed read (and is
|
|
11
|
+
* reported through the client's `onError`), instead of leaking a
|
|
12
|
+
* half-shaped product into a theme.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { IdResponseSchema, ProductListSchema, ProductSchema, ReviewListEnvelopeSchema } from "../schemas.js";
|
|
16
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
17
|
+
import { localizedPath } from "./paths.js";
|
|
18
|
+
import { reportDegradedRead } from "./core.js";
|
|
19
|
+
/**
|
|
20
|
+
* Create products namespace
|
|
21
|
+
*/
|
|
22
|
+
export function createProductsNamespace(core) {
|
|
23
|
+
return {
|
|
24
|
+
async get(handle) {
|
|
25
|
+
try {
|
|
26
|
+
const product = await core.get(localizedPath(core.config, `/store/products/${encodeURIComponent(handle)}`), ProductSchema.nullable(), { operation: STOREFRONT_OPERATIONS.productsGet });
|
|
27
|
+
return product ?? null;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
reportDegradedRead(core.config, error, "products.get");
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async list(filters) {
|
|
35
|
+
try {
|
|
36
|
+
const products = await core.get(localizedPath(core.config, "/store/products", {
|
|
37
|
+
categoryId: filters?.categoryId || undefined,
|
|
38
|
+
featured: filters?.featured ? "true" : undefined,
|
|
39
|
+
limit: filters?.limit === undefined ? undefined : String(filters.limit),
|
|
40
|
+
offset: filters?.offset ? String(filters.offset) : undefined,
|
|
41
|
+
}), z.array(ProductListSchema), { operation: STOREFRONT_OPERATIONS.productsList });
|
|
42
|
+
return products ?? [];
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
reportDegradedRead(core.config, error, "products.list");
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
reviews: {
|
|
50
|
+
async list(productId, pagination) {
|
|
51
|
+
try {
|
|
52
|
+
const params = new URLSearchParams({
|
|
53
|
+
productId,
|
|
54
|
+
limit: String(pagination?.limit ?? 20),
|
|
55
|
+
offset: String(pagination?.offset ?? 0),
|
|
56
|
+
});
|
|
57
|
+
// The server returns { data: Review[], total } — total lives
|
|
58
|
+
// outside `data`, so read the full envelope.
|
|
59
|
+
const envelope = await core.get(`/store/reviews?${params}`, ReviewListEnvelopeSchema, {
|
|
60
|
+
envelope: true,
|
|
61
|
+
operation: STOREFRONT_OPERATIONS.productsReviewsList,
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
rows: envelope?.data ?? [],
|
|
65
|
+
total: envelope?.total ?? 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
reportDegradedRead(core.config, error, "products.reviews.list");
|
|
70
|
+
return { rows: [], total: 0 };
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
async submit(data) {
|
|
74
|
+
return core.post("/store/reviews", data, { schema: IdResponseSchema, operation: STOREFRONT_OPERATIONS.productsReviewsSubmit });
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipping Namespace - Delivery Pricing
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction and the per-wilaya lookup behind two
|
|
5
|
+
* calls. The server always returns the full per-wilaya map; `rate()` extracts
|
|
6
|
+
* a single wilaya's pair when the caller only needs one.
|
|
7
|
+
*
|
|
8
|
+
* Fetched fresh every request (merchants adjust delivery pricing and expect
|
|
9
|
+
* the next render to show it).
|
|
10
|
+
*
|
|
11
|
+
* Failure contract: resolves to `{}` /
|
|
12
|
+
* `null` on any failure — never throws. Absent wilayas have no rule and are
|
|
13
|
+
* treated as unsupported.
|
|
14
|
+
*/
|
|
15
|
+
import { type ClientCore } from "./core.js";
|
|
16
|
+
import type { ShippingRates, WilayaRates } from "../types.js";
|
|
17
|
+
export interface ShippingNamespace {
|
|
18
|
+
/**
|
|
19
|
+
* All per-wilaya shipping rates, keyed by wilaya ID (as string).
|
|
20
|
+
* Resolves to `{}` when the rates cannot be fetched.
|
|
21
|
+
*/
|
|
22
|
+
rates(): Promise<ShippingRates>;
|
|
23
|
+
/**
|
|
24
|
+
* The rate pair for one wilaya.
|
|
25
|
+
* Resolves to `null` when that wilaya has no rule or fetch fails.
|
|
26
|
+
*
|
|
27
|
+
* Takes the same `number | string` as `geo.communes()`: a form control
|
|
28
|
+
* hands over a string, a typed row hands over a number, and neither caller
|
|
29
|
+
* should have to remember which of the two this one wants.
|
|
30
|
+
*/
|
|
31
|
+
rate(wilayaId: number | string): Promise<WilayaRates | null>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create shipping namespace
|
|
35
|
+
*/
|
|
36
|
+
export declare function createShippingNamespace(core: ClientCore): ShippingNamespace;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipping Namespace - Delivery Pricing
|
|
3
|
+
*
|
|
4
|
+
* Deep module: hides URL construction and the per-wilaya lookup behind two
|
|
5
|
+
* calls. The server always returns the full per-wilaya map; `rate()` extracts
|
|
6
|
+
* a single wilaya's pair when the caller only needs one.
|
|
7
|
+
*
|
|
8
|
+
* Fetched fresh every request (merchants adjust delivery pricing and expect
|
|
9
|
+
* the next render to show it).
|
|
10
|
+
*
|
|
11
|
+
* Failure contract: resolves to `{}` /
|
|
12
|
+
* `null` on any failure — never throws. Absent wilayas have no rule and are
|
|
13
|
+
* treated as unsupported.
|
|
14
|
+
*/
|
|
15
|
+
import { ShippingRatesSchema } from "../schemas.js";
|
|
16
|
+
import { STOREFRONT_OPERATIONS } from "../domain.js";
|
|
17
|
+
import { reportDegradedRead } from "./core.js";
|
|
18
|
+
/**
|
|
19
|
+
* Create shipping namespace
|
|
20
|
+
*/
|
|
21
|
+
export function createShippingNamespace(core) {
|
|
22
|
+
return {
|
|
23
|
+
async rates() {
|
|
24
|
+
try {
|
|
25
|
+
return ((await core.get("/store/shipping-rates", ShippingRatesSchema.nullable(), {
|
|
26
|
+
operation: STOREFRONT_OPERATIONS.shippingRates,
|
|
27
|
+
})) ?? {});
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
reportDegradedRead(core.config, error, "shipping.rates");
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async rate(wilayaId) {
|
|
35
|
+
try {
|
|
36
|
+
const rates = (await core.get("/store/shipping-rates", ShippingRatesSchema.nullable(), {
|
|
37
|
+
operation: STOREFRONT_OPERATIONS.shippingRate,
|
|
38
|
+
})) ?? {};
|
|
39
|
+
return rates[String(wilayaId)] ?? null;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
reportDegradedRead(core.config, error, "shipping.rate");
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|