@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
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ProductSchema, ProductListSchema, CategorySchema, ProductImageSchema, ProductVariantSchema, OfferSchema, PageDocumentPayloadSchema } from "./schemas.js";
|
|
3
|
+
export type Product = z.infer<typeof ProductSchema>;
|
|
4
|
+
export type ProductList = z.infer<typeof ProductListSchema>;
|
|
5
|
+
/** Compact catalog projection used in grids, cards and search results. */
|
|
6
|
+
export type ProductListItem = ProductList;
|
|
7
|
+
/** Complete catalog projection used on a product detail surface. */
|
|
8
|
+
export type ProductDetail = Product;
|
|
9
|
+
export type Category = z.infer<typeof CategorySchema>;
|
|
10
|
+
export type ProductImage = z.infer<typeof ProductImageSchema>;
|
|
11
|
+
export type ProductVariant = z.infer<typeof ProductVariantSchema>;
|
|
12
|
+
export type Offer = z.infer<typeof OfferSchema>;
|
|
13
|
+
/**
|
|
14
|
+
* One store's saved composition document for one surface. Transport shape
|
|
15
|
+
* only — `sections` items are `unknown`; the theme
|
|
16
|
+
* SDK's `parsePageData` validates what they mean.
|
|
17
|
+
*/
|
|
18
|
+
export type PageDocumentPayload = z.infer<typeof PageDocumentPayloadSchema>;
|
|
19
|
+
/**
|
|
20
|
+
* The merchant's order-form configuration, as the server resolves it.
|
|
21
|
+
*
|
|
22
|
+
* A plain type, not a schema: the server has already applied the defaults and
|
|
23
|
+
* validated every value by the time this arrives, and it enforces all of it
|
|
24
|
+
* again on POST /store/orders. The theme's job is to render what it is told —
|
|
25
|
+
* a theme that decided anything about form rules would be engine logic in a
|
|
26
|
+
* swappable layer, and the two copies would drift.
|
|
27
|
+
*
|
|
28
|
+
* `address` may be "hidden" (some merchants take it by phone after the order).
|
|
29
|
+
* A hidden field ships no markup at all; whether a HOME parcel may then leave
|
|
30
|
+
* without an address is a platform decision, enforced at dispatch — not here.
|
|
31
|
+
*/
|
|
32
|
+
export interface CheckoutFormPolicy {
|
|
33
|
+
address: "required" | "optional" | "hidden";
|
|
34
|
+
notes: "optional" | "hidden";
|
|
35
|
+
email: "required" | "optional" | "hidden";
|
|
36
|
+
deliveryOptions: {
|
|
37
|
+
home: boolean;
|
|
38
|
+
stopDesk: boolean;
|
|
39
|
+
};
|
|
40
|
+
customFields: CheckoutFormCustomField[];
|
|
41
|
+
}
|
|
42
|
+
export interface CheckoutFormCustomField {
|
|
43
|
+
/** Server-minted. Answers are keyed by it, so it outlives label edits. */
|
|
44
|
+
id: string;
|
|
45
|
+
/** Merchant-authored text. Render as a text node — never as markup. */
|
|
46
|
+
label: string;
|
|
47
|
+
type: "text" | "textarea" | "number" | "select";
|
|
48
|
+
required: boolean;
|
|
49
|
+
/** `select` only. */
|
|
50
|
+
options?: string[] | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** A language the store serves (`StoreConfig.locales`). */
|
|
53
|
+
export interface StorefrontLanguage {
|
|
54
|
+
/** BCP 47 code, e.g. `ar`, `fr`. */
|
|
55
|
+
code: string;
|
|
56
|
+
/** The language's name in itself, for a language switcher. */
|
|
57
|
+
nativeName: string;
|
|
58
|
+
/** `<html dir>` for this language. */
|
|
59
|
+
direction: "rtl" | "ltr";
|
|
60
|
+
}
|
|
61
|
+
export interface StoreConfig {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
domain: string | null;
|
|
65
|
+
logoUrl: string | null;
|
|
66
|
+
themeId: string;
|
|
67
|
+
primaryColor: string;
|
|
68
|
+
accentColor: string;
|
|
69
|
+
bgColor: string;
|
|
70
|
+
fontFamily: string;
|
|
71
|
+
fontUrl: string | null;
|
|
72
|
+
/** The store's primary language: a BCP 47 code such as `ar`. */
|
|
73
|
+
lang: string;
|
|
74
|
+
/**
|
|
75
|
+
* The language this config was served in: the client's `locale` when the
|
|
76
|
+
* store publishes it, `lang` otherwise.
|
|
77
|
+
*/
|
|
78
|
+
locale: string;
|
|
79
|
+
/** The published languages, primary first (always includes `lang`). */
|
|
80
|
+
locales: StorefrontLanguage[];
|
|
81
|
+
/**
|
|
82
|
+
* The store's edits to this theme's text, one map per published language:
|
|
83
|
+
* `{ ar: { heroTitle: "…" } }`. Sent only to a client created with `theme`.
|
|
84
|
+
* A theme merges `contentOverrides[locale]` over its own pack for that
|
|
85
|
+
* language — never another language's map.
|
|
86
|
+
*/
|
|
87
|
+
contentOverrides: Record<string, Record<string, string>>;
|
|
88
|
+
currency: string;
|
|
89
|
+
currencySymbol: string;
|
|
90
|
+
metaTitle: string | null;
|
|
91
|
+
metaDescription: string | null;
|
|
92
|
+
ogImage: string | null;
|
|
93
|
+
announcementBar: string | null;
|
|
94
|
+
reviewsEnabled: boolean;
|
|
95
|
+
/** When true the storefront renders the cart alongside the direct order form. */
|
|
96
|
+
cartEnabled: boolean;
|
|
97
|
+
/** Subtotal (DZD) at or above which delivery is free. null = no threshold. */
|
|
98
|
+
freeShippingThreshold: number | null;
|
|
99
|
+
cartShippingMode: "highest" | "default_profile";
|
|
100
|
+
otpEnabled: boolean;
|
|
101
|
+
/** Cloudflare Turnstile — true only when a store_turnstile_config row exists AND is enabled. */
|
|
102
|
+
turnstileEnabled: boolean;
|
|
103
|
+
/** Public widget site key; null when Turnstile is disabled. The secret never leaves the server. */
|
|
104
|
+
turnstileSiteKey: string | null;
|
|
105
|
+
status: "active" | "inactive";
|
|
106
|
+
createdAt: string;
|
|
107
|
+
updatedAt: string;
|
|
108
|
+
checkoutForm: CheckoutFormPolicy;
|
|
109
|
+
pixelId: string | null;
|
|
110
|
+
conversionEvent: "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered" | "Lead";
|
|
111
|
+
/**
|
|
112
|
+
* Published pages that opt into the footer, titled in the store's own
|
|
113
|
+
* language, ordered for display. The checkout consent line resolves Terms/
|
|
114
|
+
* Refund from here by `kind` — never a hardcoded slug, since a merchant may
|
|
115
|
+
* rename any page's slug freely.
|
|
116
|
+
*/
|
|
117
|
+
pages: StorePageLink[];
|
|
118
|
+
/**
|
|
119
|
+
* The public subset of the store's legal profile. Null until the merchant
|
|
120
|
+
* saves one from the dashboard (Settings → Store Pages) — RC/NIF are never
|
|
121
|
+
* exposed here, only inside the legal documents themselves.
|
|
122
|
+
*/
|
|
123
|
+
legalContact: StoreLegalContact | null;
|
|
124
|
+
/** The WhatsApp contact widget, already resolved, or null when disabled. */
|
|
125
|
+
whatsapp: StoreWhatsAppWidget | null;
|
|
126
|
+
}
|
|
127
|
+
/** The merchant's per-surface switches for the WhatsApp contact widget. */
|
|
128
|
+
export type WidgetSurfaces = StoreWhatsAppWidget["surfaces"];
|
|
129
|
+
export interface StoreWhatsAppWidget {
|
|
130
|
+
href: string;
|
|
131
|
+
agentName: string | null;
|
|
132
|
+
caption: string | null;
|
|
133
|
+
avatarUrl: string | null;
|
|
134
|
+
welcomeMessage: string | null;
|
|
135
|
+
launcherLabel: string | null;
|
|
136
|
+
ctaLabel: string | null;
|
|
137
|
+
prefillGeneral: string | null;
|
|
138
|
+
prefillProduct: string | null;
|
|
139
|
+
/** Button label for the thank-you surface's WhatsApp CTA. */
|
|
140
|
+
thankYouButtonLabel: string | null;
|
|
141
|
+
/** Message prefill for the thank-you surface's WhatsApp CTA; supports `{order}`. */
|
|
142
|
+
prefillThankYou: string | null;
|
|
143
|
+
accent: "whatsapp" | "primary";
|
|
144
|
+
position: "right" | "left";
|
|
145
|
+
attention: boolean;
|
|
146
|
+
surfaces: {
|
|
147
|
+
home: boolean;
|
|
148
|
+
catalog: boolean;
|
|
149
|
+
product: boolean;
|
|
150
|
+
pages: boolean;
|
|
151
|
+
thankYou: boolean;
|
|
152
|
+
checkout: boolean;
|
|
153
|
+
landing: boolean;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export interface StorePageLink {
|
|
157
|
+
id: string;
|
|
158
|
+
kind: "terms" | "privacy" | "refund" | "shipping" | "custom";
|
|
159
|
+
slug: string;
|
|
160
|
+
title: string;
|
|
161
|
+
position: number;
|
|
162
|
+
}
|
|
163
|
+
export interface StoreLegalContact {
|
|
164
|
+
contactEmail: string | null;
|
|
165
|
+
contactPhone: string | null;
|
|
166
|
+
deliveryMinDays: number;
|
|
167
|
+
deliveryMaxDays: number;
|
|
168
|
+
}
|
|
169
|
+
/** A resolved store page — GET /store/pages/{slug}. */
|
|
170
|
+
export interface StorePagePublic {
|
|
171
|
+
id: string;
|
|
172
|
+
kind: "terms" | "privacy" | "refund" | "shipping" | "custom";
|
|
173
|
+
slug: string;
|
|
174
|
+
locale: string;
|
|
175
|
+
title: string;
|
|
176
|
+
/** Sanitised HTML. Render with set:html and never re-sanitise. */
|
|
177
|
+
bodyHtml: string;
|
|
178
|
+
metaTitle: string | null;
|
|
179
|
+
metaDescription: string | null;
|
|
180
|
+
}
|
|
181
|
+
export interface ShippingRates {
|
|
182
|
+
[wilayaId: string]: WilayaRates;
|
|
183
|
+
}
|
|
184
|
+
/** Delivery pricing for one wilaya. */
|
|
185
|
+
export interface WilayaRates {
|
|
186
|
+
home: number;
|
|
187
|
+
stopDesk: number;
|
|
188
|
+
}
|
|
189
|
+
/** One image in a landing page's ordered stack. */
|
|
190
|
+
export interface LandingPageImage {
|
|
191
|
+
id: string;
|
|
192
|
+
src: string;
|
|
193
|
+
altText: string | null;
|
|
194
|
+
position: number;
|
|
195
|
+
/** Intrinsic dimensions (px), captured at upload — the page reserves the box. */
|
|
196
|
+
width: number | null;
|
|
197
|
+
height: number | null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* A published landing page — GET /store/landing-pages/{slug}.
|
|
201
|
+
*
|
|
202
|
+
* The ordered image stack,
|
|
203
|
+
* spacing, and the linked product in full store-product shape so the order
|
|
204
|
+
* form works unmodified. Draft/archived/unknown slugs resolve to null —
|
|
205
|
+
* the theme redirects to the store home, never a dead end.
|
|
206
|
+
*/
|
|
207
|
+
export interface LandingPage {
|
|
208
|
+
id: string;
|
|
209
|
+
slug: string;
|
|
210
|
+
name: string;
|
|
211
|
+
status: "draft" | "published" | "archived";
|
|
212
|
+
/** Pixels between stacked images. */
|
|
213
|
+
imageGap: number;
|
|
214
|
+
metaTitle: string | null;
|
|
215
|
+
metaDescription: string | null;
|
|
216
|
+
publishedAt: string | null;
|
|
217
|
+
/** The image stack, ordered by position (1 = top). */
|
|
218
|
+
images: LandingPageImage[];
|
|
219
|
+
/** Null when the product is no longer publicly visible. */
|
|
220
|
+
product: Product | null;
|
|
221
|
+
/**
|
|
222
|
+
* Which Meta pixel THIS page loads and fires at, already resolved
|
|
223
|
+
* server-side — the browser never works this out for itself, so it cannot
|
|
224
|
+
* disagree with the Conversions API mirror.
|
|
225
|
+
*/
|
|
226
|
+
tracking: {
|
|
227
|
+
pixelId: string | null;
|
|
228
|
+
conversionEvent: "Lead" | "Purchase" | "Purchase_Confirmed" | "Purchase_Delivered";
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
export interface Wilaya {
|
|
232
|
+
id: number;
|
|
233
|
+
name: string;
|
|
234
|
+
nameAr: string;
|
|
235
|
+
}
|
|
236
|
+
export interface Commune {
|
|
237
|
+
id: string;
|
|
238
|
+
name: string;
|
|
239
|
+
nameAr: string;
|
|
240
|
+
}
|
|
241
|
+
/** One re-priced basket line from POST /store/cart/validate. */
|
|
242
|
+
export interface CartValidationLine {
|
|
243
|
+
productId: string;
|
|
244
|
+
variantId: string | null;
|
|
245
|
+
variantLabel: string | null;
|
|
246
|
+
productName: string;
|
|
247
|
+
quantity: number;
|
|
248
|
+
/** CURRENT catalog price — the cart's optimistic price is display-only. */
|
|
249
|
+
unitPrice: number;
|
|
250
|
+
lineTotal: number;
|
|
251
|
+
/** Units still available; null when the product is not stock-tracked. */
|
|
252
|
+
maxQuantity: number | null;
|
|
253
|
+
/**
|
|
254
|
+
* Why the line cannot be ordered as requested. Blocked lines are excluded
|
|
255
|
+
* from subtotal, offers and free-delivery math server-side.
|
|
256
|
+
*/
|
|
257
|
+
blocker: "missing" | "unlisted" | "out_of_stock" | null;
|
|
258
|
+
}
|
|
259
|
+
/** A satisfied Buy-X-Get-Y reward, appended server-side at order time. */
|
|
260
|
+
export interface CartReward {
|
|
261
|
+
offerId: string;
|
|
262
|
+
productId: string | null;
|
|
263
|
+
productName: string | null;
|
|
264
|
+
quantity: number;
|
|
265
|
+
}
|
|
266
|
+
/** The catalog's truth about a basket, right now. Read-only — nothing is written. */
|
|
267
|
+
export interface CartValidation {
|
|
268
|
+
lines: CartValidationLine[];
|
|
269
|
+
subtotal: number;
|
|
270
|
+
rewards: CartReward[];
|
|
271
|
+
/** Subtotal-based, so answerable before the shopper types an address. */
|
|
272
|
+
freeDelivery: {
|
|
273
|
+
fromOffer: boolean;
|
|
274
|
+
threshold: number | null;
|
|
275
|
+
qualified: boolean;
|
|
276
|
+
remaining: number;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** What POST /store/orders returns on 201. */
|
|
280
|
+
export interface CreatedOrder {
|
|
281
|
+
orderId: string;
|
|
282
|
+
orderNumber: string;
|
|
283
|
+
price: number;
|
|
284
|
+
deliveryFee: number;
|
|
285
|
+
/** price + deliveryFee, after any offer. */
|
|
286
|
+
total: number;
|
|
287
|
+
}
|
|
288
|
+
/** One basket line in a create-order body. */
|
|
289
|
+
export interface CreateOrderItemInput {
|
|
290
|
+
productId: string;
|
|
291
|
+
productName: string;
|
|
292
|
+
variantId?: string | undefined;
|
|
293
|
+
variantLabel?: string | undefined;
|
|
294
|
+
quantity: number;
|
|
295
|
+
/**
|
|
296
|
+
* Display-only continuity value. The server re-prices every line from the
|
|
297
|
+
* catalog row; a price sent from the browser is never charged.
|
|
298
|
+
*/
|
|
299
|
+
pricePerUnit?: number | undefined;
|
|
300
|
+
/** Explicit offer tier the shopper selected. */
|
|
301
|
+
offerId?: string | undefined;
|
|
302
|
+
}
|
|
303
|
+
/** One per-unit variant selection, for multi-unit orders. */
|
|
304
|
+
export interface CreateOrderVariantSelection {
|
|
305
|
+
variantId: string;
|
|
306
|
+
variantLabel?: string | undefined;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The create-order body the server accepts.
|
|
310
|
+
*
|
|
311
|
+
* An order is either a basket or a single product, never neither: `items[]`
|
|
312
|
+
* supersedes the flat `productId`/`productName`/`pricePerUnit`, and without
|
|
313
|
+
* `items[]` all three are required. The server re-checks every rule (bounds,
|
|
314
|
+
* sellability, OTP/Turnstile proof, checkout-form policy) — this type states
|
|
315
|
+
* what may travel, not what will be accepted.
|
|
316
|
+
*
|
|
317
|
+
* `phone` may be any shape a shopper types; the server normalises it to the
|
|
318
|
+
* canonical local `05XXXXXXXX` form before deduplicating customers.
|
|
319
|
+
*/
|
|
320
|
+
export interface CreateOrderInput {
|
|
321
|
+
customerName: string;
|
|
322
|
+
phone: string;
|
|
323
|
+
wilayaId: number;
|
|
324
|
+
communeId: string;
|
|
325
|
+
address?: string | undefined;
|
|
326
|
+
deliveryType?: "home" | "stop_desk" | undefined;
|
|
327
|
+
/** The one product a direct order form was rendered for. */
|
|
328
|
+
productId?: string | undefined;
|
|
329
|
+
productName?: string | undefined;
|
|
330
|
+
variantId?: string | undefined;
|
|
331
|
+
variantLabel?: string | undefined;
|
|
332
|
+
quantity?: number | undefined;
|
|
333
|
+
/** Display-only, like the basket line's. */
|
|
334
|
+
pricePerUnit?: number | undefined;
|
|
335
|
+
notes?: string | undefined;
|
|
336
|
+
email?: string | undefined;
|
|
337
|
+
/** Answers to the merchant's custom checkout fields, keyed by field id. */
|
|
338
|
+
customFieldResponses?: string | unknown[] | undefined;
|
|
339
|
+
offerId?: string | undefined;
|
|
340
|
+
/** Meta click/browser cookies captured at placement time. */
|
|
341
|
+
fbc?: string | undefined;
|
|
342
|
+
fbp?: string | undefined;
|
|
343
|
+
/** WhatsApp OTP proof — required only while the store has OTP enabled. */
|
|
344
|
+
otpToken?: string | undefined;
|
|
345
|
+
/** Cloudflare Turnstile proof — required only while Turnstile is enabled. */
|
|
346
|
+
turnstileToken?: string | undefined;
|
|
347
|
+
variantSelections?: CreateOrderVariantSelection[] | undefined;
|
|
348
|
+
items?: CreateOrderItemInput[] | undefined;
|
|
349
|
+
/** Landing-page attribution — best-effort; an unknown slug never blocks. */
|
|
350
|
+
landingPageSlug?: string | undefined;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The order's tracking destination — which Meta pixel it belongs to and
|
|
354
|
+
* which browser event the thank-you page may fire. `event` is null when no
|
|
355
|
+
* browser event should fire (tracking off, or the merchant's conversion
|
|
356
|
+
* fires server-side further down the funnel).
|
|
357
|
+
*/
|
|
358
|
+
export interface OrderTracking {
|
|
359
|
+
pixelId: string | null;
|
|
360
|
+
event: "Purchase" | "Lead" | null;
|
|
361
|
+
}
|
|
362
|
+
/** POST /store/otp/send success payload. */
|
|
363
|
+
export type OtpSendResult = {
|
|
364
|
+
status: "sent";
|
|
365
|
+
requestId: string;
|
|
366
|
+
/** Unix milliseconds. */
|
|
367
|
+
expiresAt: number;
|
|
368
|
+
maxAttempts: number;
|
|
369
|
+
} | {
|
|
370
|
+
/** Fail-open: checkout proceeds unverified with the bypass token. */
|
|
371
|
+
status: "unavailable";
|
|
372
|
+
reason: string;
|
|
373
|
+
bypassToken: string;
|
|
374
|
+
};
|
|
375
|
+
/** POST /store/otp/verify success payload. */
|
|
376
|
+
export type OtpVerifyResult = {
|
|
377
|
+
status: "verified";
|
|
378
|
+
/** Signed, 15-min TTL, bound to the phone. Travels with the order. */
|
|
379
|
+
otpToken: string;
|
|
380
|
+
};
|
|
381
|
+
export interface Review {
|
|
382
|
+
id: string;
|
|
383
|
+
customerName: string;
|
|
384
|
+
rating: number;
|
|
385
|
+
title: string | null;
|
|
386
|
+
body: string;
|
|
387
|
+
createdAt: string;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Filters `products.list()` accepts — named so a theme can type its own
|
|
391
|
+
* wrappers and route query parsing without re-declaring the shape.
|
|
392
|
+
*/
|
|
393
|
+
export interface ProductListFilters {
|
|
394
|
+
categoryId?: string;
|
|
395
|
+
featured?: boolean;
|
|
396
|
+
/** Server-side cap; the server clamps anything larger. */
|
|
397
|
+
limit?: number;
|
|
398
|
+
/** Products to skip — with `limit`, pages through the whole catalog. */
|
|
399
|
+
offset?: number;
|
|
400
|
+
}
|
|
401
|
+
/** `products.reviews.list()` paging — named for the same reason. */
|
|
402
|
+
export interface ReviewListPagination {
|
|
403
|
+
limit?: number;
|
|
404
|
+
offset?: number;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The body `products.reviews.submit()` accepts: the order number is the proof
|
|
408
|
+
* of purchase, and the server owns every other rule (moderation, one-review
|
|
409
|
+
* per order line).
|
|
410
|
+
*/
|
|
411
|
+
export interface ReviewInput {
|
|
412
|
+
orderNumber: string;
|
|
413
|
+
productId: string;
|
|
414
|
+
/** 1–5, the server validates the range. */
|
|
415
|
+
rating: number;
|
|
416
|
+
title?: string;
|
|
417
|
+
body: string;
|
|
418
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Image Resizing Utility
|
|
3
|
+
*
|
|
4
|
+
* Generates optimized image URLs using Cloudflare's Image Resizing service.
|
|
5
|
+
* Automatically resizes images to exact dimensions and converts to WebP format.
|
|
6
|
+
*
|
|
7
|
+
* Deep module: hides Cloudflare CDN-CGI URL construction, fit modes, format
|
|
8
|
+
* negotiation, and no-op fallback logic behind a simple interface.
|
|
9
|
+
*/
|
|
10
|
+
export interface ImageResizeOptions {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
quality?: number;
|
|
14
|
+
format?: "webp" | "avif" | "auto";
|
|
15
|
+
fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate an optimized image URL using Cloudflare Image Resizing
|
|
19
|
+
*
|
|
20
|
+
* @param originalUrl - The original image URL from R2
|
|
21
|
+
* @param options - Resize and optimization options
|
|
22
|
+
* @param mediaDomain - Media domain (e.g., "media.example.com" or "https://media.example.com")
|
|
23
|
+
* @returns Optimized image URL
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const url = optimizeImage(
|
|
28
|
+
* 'https://media.example.com/products/image.jpg',
|
|
29
|
+
* { width: 400, height: 400 },
|
|
30
|
+
* 'media.example.com'
|
|
31
|
+
* );
|
|
32
|
+
* // Returns: https://media.example.com/cdn-cgi/image/width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function optimizeImage(originalUrl: string, options: ImageResizeOptions, mediaDomain?: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Predefined image sizes for common use cases
|
|
38
|
+
*/
|
|
39
|
+
export declare const IMAGE_SIZES: {
|
|
40
|
+
readonly CARD: {
|
|
41
|
+
readonly width: 400;
|
|
42
|
+
readonly height: 400;
|
|
43
|
+
readonly quality: 85;
|
|
44
|
+
};
|
|
45
|
+
readonly GALLERY_MAIN: {
|
|
46
|
+
readonly width: 800;
|
|
47
|
+
readonly height: 800;
|
|
48
|
+
readonly quality: 90;
|
|
49
|
+
};
|
|
50
|
+
readonly GALLERY_THUMB: {
|
|
51
|
+
readonly width: 80;
|
|
52
|
+
readonly height: 80;
|
|
53
|
+
readonly quality: 80;
|
|
54
|
+
};
|
|
55
|
+
readonly HERO: {
|
|
56
|
+
readonly width: 1200;
|
|
57
|
+
readonly height: 600;
|
|
58
|
+
readonly quality: 90;
|
|
59
|
+
};
|
|
60
|
+
readonly CATEGORY: {
|
|
61
|
+
readonly width: 300;
|
|
62
|
+
readonly height: 200;
|
|
63
|
+
readonly quality: 85;
|
|
64
|
+
};
|
|
65
|
+
readonly LOGO: {
|
|
66
|
+
readonly width: 200;
|
|
67
|
+
readonly height: 80;
|
|
68
|
+
readonly quality: 90;
|
|
69
|
+
readonly fit: "contain";
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Quick helper functions for common image sizes
|
|
74
|
+
*
|
|
75
|
+
* These take the media domain per call. Theme code usually binds it once with
|
|
76
|
+
* `createImageHelpers(mediaDomain)` and uses the returned helpers, so a call
|
|
77
|
+
* site can never forget the domain and silently ship an unoptimized image.
|
|
78
|
+
*/
|
|
79
|
+
export declare const getCardImage: (url: string, mediaDomain?: string) => string;
|
|
80
|
+
export declare const getGalleryImage: (url: string, mediaDomain?: string) => string;
|
|
81
|
+
export declare const getThumbnailImage: (url: string, mediaDomain?: string) => string;
|
|
82
|
+
export declare const getHeroImage: (url: string, mediaDomain?: string) => string;
|
|
83
|
+
export declare const getCategoryImage: (url: string, mediaDomain?: string) => string;
|
|
84
|
+
export declare const getLogoImage: (url: string, mediaDomain?: string) => string;
|
|
85
|
+
/**
|
|
86
|
+
* Generate responsive image srcset for different screen sizes
|
|
87
|
+
*
|
|
88
|
+
* @param originalUrl - The original image URL
|
|
89
|
+
* @param baseWidth - Base width for 1x display
|
|
90
|
+
* @param baseHeight - Base height for 1x display
|
|
91
|
+
* @param mediaDomain - Media domain
|
|
92
|
+
* @returns srcset string for responsive images
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* const srcset = generateSrcSet(
|
|
97
|
+
* 'https://media.example.com/products/image.jpg',
|
|
98
|
+
* 400,
|
|
99
|
+
* 400,
|
|
100
|
+
* 'media.example.com'
|
|
101
|
+
* );
|
|
102
|
+
* // Returns: "url 1x, url 1.5x, url 2x"
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function generateSrcSet(originalUrl: string, baseWidth: number, baseHeight: number, mediaDomain?: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Bind a media domain once and get the whole image toolkit back.
|
|
108
|
+
*
|
|
109
|
+
* The single binding point for theme code: a media domain is configured once
|
|
110
|
+
* (an env var, a store setting), and every helper then takes only the URL. A
|
|
111
|
+
* per-call domain still wins, so a theme with two media hosts can override.
|
|
112
|
+
*
|
|
113
|
+
* With no domain bound every helper is a pass-through — the correct behavior
|
|
114
|
+
* for a store that has no media domain configured (original R2 URLs).
|
|
115
|
+
*
|
|
116
|
+
* @param mediaDomain - Media domain (e.g., "media.example.com" or "https://media.example.com")
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const images = createImageHelpers(env.MEDIA_DOMAIN);
|
|
121
|
+
* images.getThumbnailImage(product.coverImage.src);
|
|
122
|
+
* images.getCardImage(url);
|
|
123
|
+
* images.generateSrcSet(url, 400, 400);
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export interface ImageHelpers {
|
|
127
|
+
/** `optimizeImage` with the media domain bound. */
|
|
128
|
+
optimizeImage: (originalUrl: string, options: ImageResizeOptions, mediaDomain?: string) => string;
|
|
129
|
+
/** `generateSrcSet` with the media domain bound. */
|
|
130
|
+
generateSrcSet: (originalUrl: string, baseWidth: number, baseHeight: number, mediaDomain?: string) => string;
|
|
131
|
+
/** The shared size presets, for callers that want the raw dimensions. */
|
|
132
|
+
IMAGE_SIZES: typeof IMAGE_SIZES;
|
|
133
|
+
getCardImage: (url: string) => string;
|
|
134
|
+
getGalleryImage: (url: string) => string;
|
|
135
|
+
getThumbnailImage: (url: string) => string;
|
|
136
|
+
getHeroImage: (url: string) => string;
|
|
137
|
+
getCategoryImage: (url: string) => string;
|
|
138
|
+
getLogoImage: (url: string) => string;
|
|
139
|
+
}
|
|
140
|
+
export declare function createImageHelpers(mediaDomain?: string): ImageHelpers;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Image Resizing Utility
|
|
3
|
+
*
|
|
4
|
+
* Generates optimized image URLs using Cloudflare's Image Resizing service.
|
|
5
|
+
* Automatically resizes images to exact dimensions and converts to WebP format.
|
|
6
|
+
*
|
|
7
|
+
* Deep module: hides Cloudflare CDN-CGI URL construction, fit modes, format
|
|
8
|
+
* negotiation, and no-op fallback logic behind a simple interface.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Generate an optimized image URL using Cloudflare Image Resizing
|
|
12
|
+
*
|
|
13
|
+
* @param originalUrl - The original image URL from R2
|
|
14
|
+
* @param options - Resize and optimization options
|
|
15
|
+
* @param mediaDomain - Media domain (e.g., "media.example.com" or "https://media.example.com")
|
|
16
|
+
* @returns Optimized image URL
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const url = optimizeImage(
|
|
21
|
+
* 'https://media.example.com/products/image.jpg',
|
|
22
|
+
* { width: 400, height: 400 },
|
|
23
|
+
* 'media.example.com'
|
|
24
|
+
* );
|
|
25
|
+
* // Returns: https://media.example.com/cdn-cgi/image/width=400,height=400,format=webp,quality=85,fit=cover/products/image.jpg
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function optimizeImage(originalUrl, options, mediaDomain) {
|
|
29
|
+
// No-op when mediaDomain is empty or URL doesn't match
|
|
30
|
+
if (!mediaDomain || !originalUrl) {
|
|
31
|
+
return originalUrl;
|
|
32
|
+
}
|
|
33
|
+
const normalizedDomain = `https://${mediaDomain.replace(/^https?:\/\//, "")}`;
|
|
34
|
+
let mediaOrigin;
|
|
35
|
+
let original;
|
|
36
|
+
try {
|
|
37
|
+
mediaOrigin = new URL(normalizedDomain).origin;
|
|
38
|
+
original = new URL(originalUrl);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return originalUrl;
|
|
42
|
+
}
|
|
43
|
+
// Only optimize a URL from the configured media origin
|
|
44
|
+
if (original.origin !== mediaOrigin) {
|
|
45
|
+
return originalUrl;
|
|
46
|
+
}
|
|
47
|
+
const { width, height, quality = 85, format = "webp", fit = "cover" } = options;
|
|
48
|
+
const imagePath = `${original.pathname.replace(/^\/+/, "")}${original.search}${original.hash}`;
|
|
49
|
+
// Build the Cloudflare Image Resizing URL
|
|
50
|
+
const resizeParams = [
|
|
51
|
+
`width=${width}`,
|
|
52
|
+
`height=${height}`,
|
|
53
|
+
`format=${format}`,
|
|
54
|
+
`quality=${quality}`,
|
|
55
|
+
`fit=${fit}`,
|
|
56
|
+
].join(",");
|
|
57
|
+
return `${mediaOrigin}/cdn-cgi/image/${resizeParams}/${imagePath}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Predefined image sizes for common use cases
|
|
61
|
+
*/
|
|
62
|
+
export const IMAGE_SIZES = {
|
|
63
|
+
// Product cards (grid view)
|
|
64
|
+
CARD: { width: 400, height: 400, quality: 85 },
|
|
65
|
+
// Product gallery main image
|
|
66
|
+
GALLERY_MAIN: { width: 800, height: 800, quality: 90 },
|
|
67
|
+
// Product gallery thumbnails
|
|
68
|
+
GALLERY_THUMB: { width: 80, height: 80, quality: 80 },
|
|
69
|
+
// Hero/banner images
|
|
70
|
+
HERO: { width: 1200, height: 600, quality: 90 },
|
|
71
|
+
// Category images
|
|
72
|
+
CATEGORY: { width: 300, height: 200, quality: 85 },
|
|
73
|
+
// Logo/brand images
|
|
74
|
+
LOGO: { width: 200, height: 80, quality: 90, fit: "contain" },
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Quick helper functions for common image sizes
|
|
78
|
+
*
|
|
79
|
+
* These take the media domain per call. Theme code usually binds it once with
|
|
80
|
+
* `createImageHelpers(mediaDomain)` and uses the returned helpers, so a call
|
|
81
|
+
* site can never forget the domain and silently ship an unoptimized image.
|
|
82
|
+
*/
|
|
83
|
+
export const getCardImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.CARD, mediaDomain);
|
|
84
|
+
export const getGalleryImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.GALLERY_MAIN, mediaDomain);
|
|
85
|
+
export const getThumbnailImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.GALLERY_THUMB, mediaDomain);
|
|
86
|
+
export const getHeroImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.HERO, mediaDomain);
|
|
87
|
+
export const getCategoryImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.CATEGORY, mediaDomain);
|
|
88
|
+
export const getLogoImage = (url, mediaDomain) => optimizeImage(url, IMAGE_SIZES.LOGO, mediaDomain);
|
|
89
|
+
/**
|
|
90
|
+
* Generate responsive image srcset for different screen sizes
|
|
91
|
+
*
|
|
92
|
+
* @param originalUrl - The original image URL
|
|
93
|
+
* @param baseWidth - Base width for 1x display
|
|
94
|
+
* @param baseHeight - Base height for 1x display
|
|
95
|
+
* @param mediaDomain - Media domain
|
|
96
|
+
* @returns srcset string for responsive images
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* const srcset = generateSrcSet(
|
|
101
|
+
* 'https://media.example.com/products/image.jpg',
|
|
102
|
+
* 400,
|
|
103
|
+
* 400,
|
|
104
|
+
* 'media.example.com'
|
|
105
|
+
* );
|
|
106
|
+
* // Returns: "url 1x, url 1.5x, url 2x"
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function generateSrcSet(originalUrl, baseWidth, baseHeight, mediaDomain) {
|
|
110
|
+
const sizes = [
|
|
111
|
+
{ width: baseWidth, height: baseHeight, density: "1x" },
|
|
112
|
+
{ width: Math.round(baseWidth * 1.5), height: Math.round(baseHeight * 1.5), density: "1.5x" },
|
|
113
|
+
{ width: baseWidth * 2, height: baseHeight * 2, density: "2x" },
|
|
114
|
+
];
|
|
115
|
+
return sizes
|
|
116
|
+
.map((size) => `${optimizeImage(originalUrl, size, mediaDomain)} ${size.density}`)
|
|
117
|
+
.join(", ");
|
|
118
|
+
}
|
|
119
|
+
export function createImageHelpers(mediaDomain) {
|
|
120
|
+
return {
|
|
121
|
+
optimizeImage: (originalUrl, options, domain = mediaDomain) => optimizeImage(originalUrl, options, domain),
|
|
122
|
+
generateSrcSet: (originalUrl, baseWidth, baseHeight, domain = mediaDomain) => generateSrcSet(originalUrl, baseWidth, baseHeight, domain),
|
|
123
|
+
IMAGE_SIZES,
|
|
124
|
+
getCardImage: (url) => getCardImage(url, mediaDomain),
|
|
125
|
+
getGalleryImage: (url) => getGalleryImage(url, mediaDomain),
|
|
126
|
+
getThumbnailImage: (url) => getThumbnailImage(url, mediaDomain),
|
|
127
|
+
getHeroImage: (url) => getHeroImage(url, mediaDomain),
|
|
128
|
+
getCategoryImage: (url) => getCategoryImage(url, mediaDomain),
|
|
129
|
+
getLogoImage: (url) => getLogoImage(url, mediaDomain),
|
|
130
|
+
};
|
|
131
|
+
}
|