@wocart/storefront 0.0.0-stage → 1.0.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 +66 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +143 -2
- package/USAGE.md +1465 -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 +86 -0
- package/dist/client/core.js +582 -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 +39 -0
- package/dist/client/index.js +80 -0
- package/dist/client/orders.d.ts +68 -0
- package/dist/client/orders.js +76 -0
- package/dist/client/page-data.d.ts +31 -0
- package/dist/client/page-data.js +37 -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 +37 -0
- package/dist/client/shipping.js +48 -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 +332 -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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Proxy Endpoint Factory
|
|
3
|
+
*
|
|
4
|
+
* Consolidates 8 separate endpoint files into ONE factory function.
|
|
5
|
+
* Hides: input parsing, validation, CSRF protection, header forwarding,
|
|
6
|
+
* error normalization, and response formatting.
|
|
7
|
+
*
|
|
8
|
+
* Deep module principle: Complex proxy logic concentrated in one place,
|
|
9
|
+
* giving callers maximum leverage with minimal interface.
|
|
10
|
+
*/
|
|
11
|
+
import type { z } from "zod";
|
|
12
|
+
/**
|
|
13
|
+
* Astro APIRoute handler type (avoiding direct Astro dependency in types)
|
|
14
|
+
*/
|
|
15
|
+
export type AstroAPIContext = {
|
|
16
|
+
request: Request;
|
|
17
|
+
params: Record<string, string | undefined>;
|
|
18
|
+
};
|
|
19
|
+
export type AstroAPIRoute = (context: AstroAPIContext) => Response | Promise<Response>;
|
|
20
|
+
/**
|
|
21
|
+
* Client function that makes the actual API call to the server.
|
|
22
|
+
*
|
|
23
|
+
* On failure, every extra field besides `success` and `error` (e.g.
|
|
24
|
+
* `code`, `windowSeconds`, `attemptsRemaining`, `terminal`) is forwarded
|
|
25
|
+
* into the error response body — the storefront UI reads these to render
|
|
26
|
+
* rate-limit countdowns and attempt counters.
|
|
27
|
+
*/
|
|
28
|
+
export type ProxyClientFn<TInput, TOutput> = (data: TInput, headers?: Record<string, string>) => Promise<{
|
|
29
|
+
success: true;
|
|
30
|
+
data: TOutput;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
} | {
|
|
33
|
+
success: false;
|
|
34
|
+
error: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for creating a proxy endpoint
|
|
39
|
+
*/
|
|
40
|
+
export interface ProxyEndpointConfig<TInput, TOutput> {
|
|
41
|
+
/** HTTP method: 'GET' or 'POST' */
|
|
42
|
+
method: "GET" | "POST";
|
|
43
|
+
/** Client function that calls the server — usually a `StoreClient` method. */
|
|
44
|
+
client: ProxyClientFn<TInput, TOutput>;
|
|
45
|
+
/** Input validation schema (optional for GET without query validation) */
|
|
46
|
+
inputSchema?: z.Schema<TInput>;
|
|
47
|
+
/**
|
|
48
|
+
* Query parameter names for GET requests
|
|
49
|
+
* Example: ['wilayaId'] will extract { wilayaId: '42' } from ?wilayaId=42
|
|
50
|
+
*/
|
|
51
|
+
queryParams?: string[];
|
|
52
|
+
/**
|
|
53
|
+
* Path parameter substitution for dynamic URLs
|
|
54
|
+
* Example: { wilayaId: 'number' } will parse params.wilayaId as number
|
|
55
|
+
*/
|
|
56
|
+
pathParams?: Record<string, "string" | "number">;
|
|
57
|
+
/**
|
|
58
|
+
* Enable CSRF protection (cross-origin check)
|
|
59
|
+
* Default: true for POST, false for GET
|
|
60
|
+
*/
|
|
61
|
+
csrfProtection?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Forward attribution headers (User-Agent, X-Forwarded-For)
|
|
64
|
+
* Default: false
|
|
65
|
+
*/
|
|
66
|
+
forwardAttribution?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Success status code (default: 200)
|
|
69
|
+
*/
|
|
70
|
+
successStatus?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Status code for upstream failures (default: 502).
|
|
73
|
+
* Typical choices: 502 for cart/abandoned/otp-send, 422 for otp-verify,
|
|
74
|
+
* 400 for submit-review.
|
|
75
|
+
*/
|
|
76
|
+
errorStatus?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Shape the success response body (default: `(data) => ({ data })`).
|
|
79
|
+
* Use when the browser contract carries more than `{data}` — e.g.
|
|
80
|
+
* `(d) => ({ success: true, data: d })` for cart validation, or
|
|
81
|
+
* `({ rows, total }) => ({ data: rows, total })` for review pagination.
|
|
82
|
+
*/
|
|
83
|
+
successBody?: (data: TOutput) => unknown;
|
|
84
|
+
/**
|
|
85
|
+
* Return 204 No Content on success instead of JSON
|
|
86
|
+
* Default: false
|
|
87
|
+
*/
|
|
88
|
+
noContent?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a proxy endpoint handler
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* // POST endpoint with validation
|
|
96
|
+
* export const POST = createProxyEndpoint({
|
|
97
|
+
* method: 'POST',
|
|
98
|
+
* client: validateCart,
|
|
99
|
+
* inputSchema: cartSchema,
|
|
100
|
+
* csrfProtection: true,
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // GET endpoint with path params
|
|
104
|
+
* export const GET = createProxyEndpoint({
|
|
105
|
+
* method: 'GET',
|
|
106
|
+
* client: fetchCommunes,
|
|
107
|
+
* pathParams: { wilayaId: 'number' },
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function createProxyEndpoint<TInput, TOutput>(config: ProxyEndpointConfig<TInput, TOutput>): AstroAPIRoute;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Proxy Endpoint Factory
|
|
3
|
+
*
|
|
4
|
+
* Consolidates 8 separate endpoint files into ONE factory function.
|
|
5
|
+
* Hides: input parsing, validation, CSRF protection, header forwarding,
|
|
6
|
+
* error normalization, and response formatting.
|
|
7
|
+
*
|
|
8
|
+
* Deep module principle: Complex proxy logic concentrated in one place,
|
|
9
|
+
* giving callers maximum leverage with minimal interface.
|
|
10
|
+
*/
|
|
11
|
+
import { collectForwardedHeaders } from "../actions.js";
|
|
12
|
+
/**
|
|
13
|
+
* Create a proxy endpoint handler
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // POST endpoint with validation
|
|
18
|
+
* export const POST = createProxyEndpoint({
|
|
19
|
+
* method: 'POST',
|
|
20
|
+
* client: validateCart,
|
|
21
|
+
* inputSchema: cartSchema,
|
|
22
|
+
* csrfProtection: true,
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // GET endpoint with path params
|
|
26
|
+
* export const GET = createProxyEndpoint({
|
|
27
|
+
* method: 'GET',
|
|
28
|
+
* client: fetchCommunes,
|
|
29
|
+
* pathParams: { wilayaId: 'number' },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createProxyEndpoint(config) {
|
|
34
|
+
const { method, client, inputSchema, queryParams = [], pathParams = {}, csrfProtection = method === "POST", forwardAttribution = false, successStatus = 200, errorStatus = 502, successBody, noContent = false, } = config;
|
|
35
|
+
return async (context) => {
|
|
36
|
+
const { request, params } = context;
|
|
37
|
+
// CSRF Protection: reject cross-origin requests
|
|
38
|
+
if (csrfProtection && isCrossOrigin(request)) {
|
|
39
|
+
return jsonResponse({ error: "Cross-origin not allowed" }, 403);
|
|
40
|
+
}
|
|
41
|
+
// Parse input based on method
|
|
42
|
+
let input;
|
|
43
|
+
try {
|
|
44
|
+
if (method === "GET") {
|
|
45
|
+
input = parseGetInput(request, params, queryParams, pathParams);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
input = await parsePostInput(request);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return jsonResponse({ error: error instanceof Error ? error.message : "Invalid input" }, 400);
|
|
53
|
+
}
|
|
54
|
+
// Validate input with Zod schema
|
|
55
|
+
if (inputSchema) {
|
|
56
|
+
const parsed = inputSchema.safeParse(input);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
return jsonResponse({ error: "Invalid payload" }, 400);
|
|
59
|
+
}
|
|
60
|
+
input = parsed.data;
|
|
61
|
+
}
|
|
62
|
+
// Prepare attribution headers
|
|
63
|
+
const headers = forwardAttribution
|
|
64
|
+
? collectForwardedHeaders(request)
|
|
65
|
+
: undefined;
|
|
66
|
+
// Call client function
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = await client(input, headers);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return jsonResponse({ error: error instanceof Error ? error.message : "Upstream request failed" }, errorStatus);
|
|
73
|
+
}
|
|
74
|
+
// Handle client errors — forward every diagnostic field the client
|
|
75
|
+
// attached (code, windowSeconds, attemptsRemaining, terminal, ...)
|
|
76
|
+
// so the storefront UI can render countdowns and attempt counters.
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
const extra = { ...result };
|
|
79
|
+
delete extra.success;
|
|
80
|
+
delete extra.error;
|
|
81
|
+
return jsonResponse({ error: result.error, ...extra }, errorStatus);
|
|
82
|
+
}
|
|
83
|
+
// Return success response
|
|
84
|
+
if (noContent) {
|
|
85
|
+
return new Response(null, { status: 204 });
|
|
86
|
+
}
|
|
87
|
+
const body = successBody ? successBody(result.data) : { data: result.data };
|
|
88
|
+
return jsonResponse(body, successStatus);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse GET request input from query params and path params
|
|
93
|
+
*/
|
|
94
|
+
function parseGetInput(request, params, queryParams, pathParams) {
|
|
95
|
+
const url = new URL(request.url);
|
|
96
|
+
const input = {};
|
|
97
|
+
// Extract query parameters
|
|
98
|
+
for (const key of queryParams) {
|
|
99
|
+
const value = url.searchParams.get(key);
|
|
100
|
+
if (value !== null) {
|
|
101
|
+
input[key] = value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Extract and parse path parameters
|
|
105
|
+
for (const [key, type] of Object.entries(pathParams)) {
|
|
106
|
+
const value = params[key];
|
|
107
|
+
if (!value) {
|
|
108
|
+
throw new Error(`Missing path parameter: ${key}`);
|
|
109
|
+
}
|
|
110
|
+
if (type === "number") {
|
|
111
|
+
const parsed = Number(value);
|
|
112
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
113
|
+
throw new Error(`Invalid number for path parameter: ${key}`);
|
|
114
|
+
}
|
|
115
|
+
input[key] = parsed;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
input[key] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return input;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse POST request JSON body
|
|
125
|
+
*/
|
|
126
|
+
async function parsePostInput(request) {
|
|
127
|
+
try {
|
|
128
|
+
return await request.json();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error("Invalid JSON body");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check if request is cross-origin
|
|
136
|
+
*
|
|
137
|
+
* Mirrors Astro Actions' built-in CSRF check:
|
|
138
|
+
* - Requests without Origin header are allowed (older browsers, curl, server-to-server)
|
|
139
|
+
* - Requests with matching origin are allowed
|
|
140
|
+
* - Requests with different origin are rejected
|
|
141
|
+
*/
|
|
142
|
+
function isCrossOrigin(request) {
|
|
143
|
+
const origin = request.headers.get("Origin");
|
|
144
|
+
if (!origin)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
return new URL(origin).origin !== new URL(request.url).origin;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Create JSON response
|
|
155
|
+
*/
|
|
156
|
+
function jsonResponse(body, status) {
|
|
157
|
+
return new Response(JSON.stringify(body), {
|
|
158
|
+
status,
|
|
159
|
+
headers: { "Content-Type": "application/json" },
|
|
160
|
+
});
|
|
161
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront SDK — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe storefront contracts and presentation utilities.
|
|
5
|
+
*
|
|
6
|
+
* Server code imports `@wocart/storefront/server` for the API-key client.
|
|
7
|
+
* Astro server code imports `@wocart/storefront/astro` for proxy and action
|
|
8
|
+
* adapters. Neither server-only entry point is part of this root surface.
|
|
9
|
+
*
|
|
10
|
+
* Internal modules stay reachable only through their intentional entry points.
|
|
11
|
+
*/
|
|
12
|
+
export { formatMoney } from "./utils/money.js";
|
|
13
|
+
export type { MoneyFormatOptions } from "./utils/money.js";
|
|
14
|
+
export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
|
|
15
|
+
export type { ImageHelpers, ImageResizeOptions } from "./utils/image.js";
|
|
16
|
+
export { productImageSource } from "./utils/product-image.js";
|
|
17
|
+
export type { ProductImageSourceInput } from "./utils/product-image.js";
|
|
18
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
|
|
19
|
+
export type { PrefillToken, ResolvePrefillParams } from "./whatsapp.js";
|
|
20
|
+
export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
|
|
21
|
+
export { servedLanguage } from "./locales.js";
|
|
22
|
+
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
23
|
+
export type { LocalePath, LocaleRouting } from "./locale-routing.js";
|
|
24
|
+
export type { StorefrontRoute } from "./routes.js";
|
|
25
|
+
export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
26
|
+
export type { BasketStore, BasketStorageAdapter, BasketStoreOptions, BasketListener, } from "./basket.js";
|
|
27
|
+
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
28
|
+
export type { OtpMachine, OtpMachineOptions, OtpFailureContext, OtpResponseOptions, OtpState, OtpStatus, OtpTransport, OtpTransportResult, OtpStateListener, } from "./otp.js";
|
|
29
|
+
export { WILAYAS } from "./data/wilayas.js";
|
|
30
|
+
export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
|
|
31
|
+
export type { CommuneTuple } from "./data/communes.js";
|
|
32
|
+
export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
|
|
33
|
+
export * from "./types.js";
|
|
34
|
+
export * from "./schemas.js";
|
|
35
|
+
export * from "./domain.js";
|
|
36
|
+
export * from "./product-view.js";
|
|
37
|
+
export { getOfferTierView } from "./offer-view.js";
|
|
38
|
+
export type { OfferTierInput, OfferTierView } from "./offer-view.js";
|
|
39
|
+
export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
|
|
40
|
+
export type { AbandonedCheckoutPorts, AbandonedCheckoutSnapshot, AbandonedCheckoutStorage, AbandonedCheckoutTracker, } from "./abandoned-checkout.js";
|
|
41
|
+
export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
|
|
42
|
+
export type { MetaCartPayload, MetaCatalogPayload, MetaContent, MetaEventName, MetaPixelFunction, MetaPixelOptions, MetaPixelTracker, } from "./meta-tracking.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storefront SDK — public entry point.
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe storefront contracts and presentation utilities.
|
|
5
|
+
*
|
|
6
|
+
* Server code imports `@wocart/storefront/server` for the API-key client.
|
|
7
|
+
* Astro server code imports `@wocart/storefront/astro` for proxy and action
|
|
8
|
+
* adapters. Neither server-only entry point is part of this root surface.
|
|
9
|
+
*
|
|
10
|
+
* Internal modules stay reachable only through their intentional entry points.
|
|
11
|
+
*/
|
|
12
|
+
export { formatMoney } from "./utils/money.js";
|
|
13
|
+
export { optimizeImage, generateSrcSet, IMAGE_SIZES, getCardImage, getGalleryImage, getThumbnailImage, getHeroImage, getCategoryImage, getLogoImage, createImageHelpers, } from "./utils/image.js";
|
|
14
|
+
export { productImageSource } from "./utils/product-image.js";
|
|
15
|
+
export { MAX_RESOLVED_PREFILL_LENGTH, PREFILL_TOKENS, stripTrackingParams, truncateAtWordBoundary, resolvePrefillText, buildWhatsAppHref, } from "./whatsapp.js";
|
|
16
|
+
export { STOREFRONT_ROUTES, showsWidgetOn } from "./routes.js";
|
|
17
|
+
export { servedLanguage } from "./locales.js";
|
|
18
|
+
export { alternateLinks, localeRouting, localizeHref, resolveLocalePath } from "./locale-routing.js";
|
|
19
|
+
export { createBasketStore, createMemoryStorageAdapter, createLocalStorageAdapter, DEFAULT_BASKET_STORAGE_KEY, toOrderItems, } from "./basket.js";
|
|
20
|
+
export { createOtpMachine, createOtpTransport, mapOtpResponse, otpTransportFailure, } from "./otp.js";
|
|
21
|
+
export { WILAYAS } from "./data/wilayas.js";
|
|
22
|
+
export { ALGERIA_COMMUNES, communesForWilaya } from "./data/communes.js";
|
|
23
|
+
export { normalizeAlgerianPhone, toLocalAlgerianMobile } from "./phone.js";
|
|
24
|
+
export * from "./types.js";
|
|
25
|
+
export * from "./schemas.js";
|
|
26
|
+
export * from "./domain.js";
|
|
27
|
+
export * from "./product-view.js";
|
|
28
|
+
export { getOfferTierView } from "./offer-view.js";
|
|
29
|
+
export { createAbandonedCheckoutTracker } from "./abandoned-checkout.js";
|
|
30
|
+
export { META_CURRENCY, buildMetaAddToCartPayload, buildMetaInitiateCheckoutPayload, buildMetaProductPayload, buildMetaViewContentPayload, createMetaPixelTracker, } from "./meta-tracking.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-prefixed storefront URLs — Shopify's
|
|
3
|
+
* subfolders: the primary language keeps today's URLs (`/products/x`), every
|
|
4
|
+
* other published language lives under its code (`/fr/products/x`), and the
|
|
5
|
+
* primary has exactly one address (`/ar/products/x` redirects to
|
|
6
|
+
* `/products/x`).
|
|
7
|
+
*
|
|
8
|
+
* Pure functions over the store's own languages (`StoreConfig.lang` and
|
|
9
|
+
* `StoreConfig.locales`): a theme reads the request's language with
|
|
10
|
+
* `resolveLocalePath`, builds every internal link with `localizeHref`, and
|
|
11
|
+
* lists a page's language versions with `alternateLinks` — the counterpart of
|
|
12
|
+
* Shopify's `request.locale.root_url` and `routes`.
|
|
13
|
+
*/
|
|
14
|
+
import type { StoreConfig } from "./types.js";
|
|
15
|
+
/** The store's languages as URLs need them. */
|
|
16
|
+
export interface LocaleRouting {
|
|
17
|
+
/** The primary language: the one without a prefix. */
|
|
18
|
+
readonly primary: string;
|
|
19
|
+
/** Every published language, primary first. */
|
|
20
|
+
readonly published: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function localeRouting(config: Pick<StoreConfig, "lang" | "locales">): LocaleRouting;
|
|
23
|
+
export type LocalePath =
|
|
24
|
+
/** Serve `pathname` in `locale`. */
|
|
25
|
+
{
|
|
26
|
+
kind: "page";
|
|
27
|
+
locale: string;
|
|
28
|
+
pathname: string;
|
|
29
|
+
}
|
|
30
|
+
/** The primary under a prefix: send the shopper to its one address. */
|
|
31
|
+
| {
|
|
32
|
+
kind: "redirect";
|
|
33
|
+
location: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Which language a request is for, and the path it renders. A first segment
|
|
37
|
+
* that is a published language other than the primary selects it; the
|
|
38
|
+
* primary's own prefix redirects; anything else — an unpublished or unknown
|
|
39
|
+
* code included — is an ordinary path in the primary language, so it renders
|
|
40
|
+
* or 404s exactly as before.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveLocalePath(pathname: string, routing: LocaleRouting): LocalePath;
|
|
43
|
+
/**
|
|
44
|
+
* An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
|
|
45
|
+
* Left unchanged: the primary language, external and protocol-relative URLs,
|
|
46
|
+
* fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
|
|
47
|
+
* carries a language prefix. Query strings and fragments are kept.
|
|
48
|
+
*/
|
|
49
|
+
export declare function localizeHref(href: string, locale: string, routing: LocaleRouting): string;
|
|
50
|
+
/**
|
|
51
|
+
* Every published language's URL for one page, primary first, plus
|
|
52
|
+
* `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
|
|
53
|
+
* `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
|
|
54
|
+
* as search engines require.
|
|
55
|
+
*/
|
|
56
|
+
export declare function alternateLinks(pathname: string, routing: LocaleRouting, origin: string): {
|
|
57
|
+
hreflang: string;
|
|
58
|
+
href: string;
|
|
59
|
+
}[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function localeRouting(config) {
|
|
2
|
+
const published = config.locales.map((language) => language.code);
|
|
3
|
+
return {
|
|
4
|
+
primary: config.lang,
|
|
5
|
+
published: published.includes(config.lang) ? published : [config.lang, ...published],
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
/** Paths that are never pages: endpoints, framework internals, files. */
|
|
9
|
+
function isPagePath(pathname) {
|
|
10
|
+
return !/^\/(?:api|_[^/]*)(?:\/|$)/.test(pathname) && !/\/[^/]*\.[a-z0-9]+$/i.test(pathname);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Which language a request is for, and the path it renders. A first segment
|
|
14
|
+
* that is a published language other than the primary selects it; the
|
|
15
|
+
* primary's own prefix redirects; anything else — an unpublished or unknown
|
|
16
|
+
* code included — is an ordinary path in the primary language, so it renders
|
|
17
|
+
* or 404s exactly as before.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveLocalePath(pathname, routing) {
|
|
20
|
+
const match = /^\/([^/]+)(\/.*)?$/.exec(pathname);
|
|
21
|
+
const segment = match?.[1];
|
|
22
|
+
if (segment !== undefined && routing.published.includes(segment)) {
|
|
23
|
+
const rest = match?.[2] ?? "/";
|
|
24
|
+
if (segment === routing.primary)
|
|
25
|
+
return { kind: "redirect", location: rest };
|
|
26
|
+
return { kind: "page", locale: segment, pathname: rest };
|
|
27
|
+
}
|
|
28
|
+
return { kind: "page", locale: routing.primary, pathname };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An internal link in `locale`: `/products/x` → `/fr/products/x`, `/` → `/fr`.
|
|
32
|
+
* Left unchanged: the primary language, external and protocol-relative URLs,
|
|
33
|
+
* fragments, `mailto:`/`tel:`, endpoints and files, and a link that already
|
|
34
|
+
* carries a language prefix. Query strings and fragments are kept.
|
|
35
|
+
*/
|
|
36
|
+
export function localizeHref(href, locale, routing) {
|
|
37
|
+
if (locale === routing.primary || !href.startsWith("/") || href.startsWith("//"))
|
|
38
|
+
return href;
|
|
39
|
+
const split = /^([^?#]*)(.*)$/.exec(href);
|
|
40
|
+
const pathname = split[1];
|
|
41
|
+
const rest = split[2];
|
|
42
|
+
if (!isPagePath(pathname))
|
|
43
|
+
return href;
|
|
44
|
+
const first = /^\/([^/]+)/.exec(pathname)?.[1];
|
|
45
|
+
if (first !== undefined && routing.published.includes(first))
|
|
46
|
+
return href;
|
|
47
|
+
return `/${locale}${pathname === "/" ? "" : pathname}${rest}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Every published language's URL for one page, primary first, plus
|
|
51
|
+
* `x-default` (the primary) — the `<link rel="alternate" hreflang>` set.
|
|
52
|
+
* `pathname` is the page's unprefixed path; `origin` makes the URLs absolute,
|
|
53
|
+
* as search engines require.
|
|
54
|
+
*/
|
|
55
|
+
export function alternateLinks(pathname, routing, origin) {
|
|
56
|
+
const base = origin.replace(/\/+$/, "");
|
|
57
|
+
const links = routing.published.map((locale) => ({
|
|
58
|
+
hreflang: locale,
|
|
59
|
+
href: `${base}${localizeHref(pathname, locale, routing)}`,
|
|
60
|
+
}));
|
|
61
|
+
return [...links, { hreflang: "x-default", href: `${base}${pathname}` }];
|
|
62
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store languages as a storefront receives them.
|
|
3
|
+
*
|
|
4
|
+
* The platform's list of supported languages lives on the server, never in
|
|
5
|
+
* this package: `/store/config` sends each served language's code, name and
|
|
6
|
+
* text direction, so a language the platform adds reaches every theme without
|
|
7
|
+
* an SDK release — as Shopify themes read `localization.available_languages`.
|
|
8
|
+
*/
|
|
9
|
+
import type { StoreConfig, StorefrontLanguage } from "./types.js";
|
|
10
|
+
/**
|
|
11
|
+
* The served language with `code` (the store's primary by default), or `null`
|
|
12
|
+
* when the store does not serve it — the shape a theme reads `<html dir>` and
|
|
13
|
+
* a language switcher's labels from.
|
|
14
|
+
*/
|
|
15
|
+
export declare function servedLanguage(config: Pick<StoreConfig, "lang" | "locales">, code?: string): StorefrontLanguage | null;
|
package/dist/locales.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The served language with `code` (the store's primary by default), or `null`
|
|
3
|
+
* when the store does not serve it — the shape a theme reads `<html dir>` and
|
|
4
|
+
* a language switcher's labels from.
|
|
5
|
+
*/
|
|
6
|
+
export function servedLanguage(config, code = config.lang) {
|
|
7
|
+
return config.locales.find((language) => language.code === code) ?? null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BasketLine } from "./domain.js";
|
|
2
|
+
export declare const META_CURRENCY: "DZD";
|
|
3
|
+
export type MetaEventName = "ViewContent" | "AddToCart" | "InitiateCheckout" | "Lead" | "Purchase";
|
|
4
|
+
export interface MetaContent {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly quantity: number;
|
|
7
|
+
readonly item_price: number;
|
|
8
|
+
}
|
|
9
|
+
export interface MetaCatalogPayload {
|
|
10
|
+
readonly [key: string]: unknown;
|
|
11
|
+
readonly content_ids: readonly string[];
|
|
12
|
+
readonly content_type: "product";
|
|
13
|
+
readonly value: number;
|
|
14
|
+
readonly currency: typeof META_CURRENCY;
|
|
15
|
+
}
|
|
16
|
+
export interface MetaCartPayload extends MetaCatalogPayload {
|
|
17
|
+
readonly contents: readonly MetaContent[];
|
|
18
|
+
readonly num_items: number;
|
|
19
|
+
}
|
|
20
|
+
export interface MetaPixelOptions {
|
|
21
|
+
readonly eventID?: string;
|
|
22
|
+
}
|
|
23
|
+
export type MetaPixelFunction = (...args: unknown[]) => void;
|
|
24
|
+
export interface MetaPixelTracker {
|
|
25
|
+
track(eventName: MetaEventName, params: Record<string, unknown>): void;
|
|
26
|
+
trackSingle(pixelId: string, eventName: MetaEventName | string, params: Record<string, unknown>, options?: MetaPixelOptions): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createMetaPixelTracker(options: {
|
|
29
|
+
readonly getPixel: () => MetaPixelFunction | null;
|
|
30
|
+
}): MetaPixelTracker;
|
|
31
|
+
export declare function buildMetaProductPayload(productId: string, value: number): MetaCatalogPayload;
|
|
32
|
+
export declare function buildMetaViewContentPayload(productId: string, value: number): MetaCatalogPayload;
|
|
33
|
+
export declare function buildMetaAddToCartPayload(lines: readonly BasketLine[]): MetaCartPayload | null;
|
|
34
|
+
export declare function buildMetaInitiateCheckoutPayload(lines: readonly BasketLine[], value: number): MetaCartPayload | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const META_CURRENCY = "DZD";
|
|
2
|
+
export function createMetaPixelTracker(options) {
|
|
3
|
+
return {
|
|
4
|
+
track(eventName, params) {
|
|
5
|
+
options.getPixel()?.("track", eventName, params);
|
|
6
|
+
},
|
|
7
|
+
trackSingle(pixelId, eventName, params, eventOptions) {
|
|
8
|
+
if (!pixelId)
|
|
9
|
+
return;
|
|
10
|
+
const pixel = options.getPixel();
|
|
11
|
+
if (!pixel)
|
|
12
|
+
return;
|
|
13
|
+
if (eventOptions) {
|
|
14
|
+
pixel("trackSingle", pixelId, eventName, params, eventOptions);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
pixel("trackSingle", pixelId, eventName, params);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function contentsOf(lines) {
|
|
23
|
+
return lines.map((line) => ({
|
|
24
|
+
id: line.productId,
|
|
25
|
+
quantity: line.quantity,
|
|
26
|
+
item_price: line.unitPrice,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function idsOf(lines) {
|
|
30
|
+
return [...new Set(lines.map((line) => line.productId))];
|
|
31
|
+
}
|
|
32
|
+
function countOf(lines) {
|
|
33
|
+
return lines.reduce((total, line) => total + line.quantity, 0);
|
|
34
|
+
}
|
|
35
|
+
function subtotalOf(lines) {
|
|
36
|
+
return lines.reduce((total, line) => total + line.unitPrice * line.quantity, 0);
|
|
37
|
+
}
|
|
38
|
+
export function buildMetaProductPayload(productId, value) {
|
|
39
|
+
return {
|
|
40
|
+
content_ids: [productId],
|
|
41
|
+
content_type: "product",
|
|
42
|
+
value,
|
|
43
|
+
currency: META_CURRENCY,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function buildMetaViewContentPayload(productId, value) {
|
|
47
|
+
return buildMetaProductPayload(productId, value);
|
|
48
|
+
}
|
|
49
|
+
export function buildMetaAddToCartPayload(lines) {
|
|
50
|
+
if (lines.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
content_ids: idsOf(lines),
|
|
54
|
+
content_type: "product",
|
|
55
|
+
contents: contentsOf(lines),
|
|
56
|
+
num_items: countOf(lines),
|
|
57
|
+
value: subtotalOf(lines),
|
|
58
|
+
currency: META_CURRENCY,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function buildMetaInitiateCheckoutPayload(lines, value) {
|
|
62
|
+
if (lines.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
content_ids: idsOf(lines),
|
|
66
|
+
content_type: "product",
|
|
67
|
+
contents: contentsOf(lines),
|
|
68
|
+
num_items: countOf(lines),
|
|
69
|
+
value,
|
|
70
|
+
currency: META_CURRENCY,
|
|
71
|
+
};
|
|
72
|
+
}
|