@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +144 -2
  5. package/USAGE.md +1470 -0
  6. package/dist/abandoned-checkout.d.ts +22 -0
  7. package/dist/abandoned-checkout.js +89 -0
  8. package/dist/actions.d.ts +117 -0
  9. package/dist/actions.js +196 -0
  10. package/dist/astro.d.ts +6 -0
  11. package/dist/astro.js +3 -0
  12. package/dist/basket.d.ts +87 -0
  13. package/dist/basket.js +258 -0
  14. package/dist/client/cart.d.ts +30 -0
  15. package/dist/client/cart.js +26 -0
  16. package/dist/client/categories.d.ts +25 -0
  17. package/dist/client/categories.js +37 -0
  18. package/dist/client/config.d.ts +29 -0
  19. package/dist/client/config.js +41 -0
  20. package/dist/client/core.d.ts +88 -0
  21. package/dist/client/core.js +566 -0
  22. package/dist/client/errors.d.ts +19 -0
  23. package/dist/client/errors.js +31 -0
  24. package/dist/client/geo.d.ts +31 -0
  25. package/dist/client/geo.js +39 -0
  26. package/dist/client/index.d.ts +40 -0
  27. package/dist/client/index.js +81 -0
  28. package/dist/client/orders.d.ts +68 -0
  29. package/dist/client/orders.js +76 -0
  30. package/dist/client/page-data.d.ts +30 -0
  31. package/dist/client/page-data.js +36 -0
  32. package/dist/client/pages.d.ts +33 -0
  33. package/dist/client/pages.js +44 -0
  34. package/dist/client/paths.d.ts +8 -0
  35. package/dist/client/paths.js +10 -0
  36. package/dist/client/products.d.ts +56 -0
  37. package/dist/client/products.js +78 -0
  38. package/dist/client/shipping.d.ts +36 -0
  39. package/dist/client/shipping.js +47 -0
  40. package/dist/data/communes.d.ts +25 -0
  41. package/dist/data/communes.js +1690 -0
  42. package/dist/data/wilayas.d.ts +6 -0
  43. package/dist/data/wilayas.js +64 -0
  44. package/dist/domain.d.ts +341 -0
  45. package/dist/domain.js +214 -0
  46. package/dist/endpoints/proxy.d.ts +111 -0
  47. package/dist/endpoints/proxy.js +161 -0
  48. package/dist/index.d.ts +42 -0
  49. package/dist/index.js +30 -0
  50. package/dist/locale-routing.d.ts +59 -0
  51. package/dist/locale-routing.js +62 -0
  52. package/dist/locales.d.ts +15 -0
  53. package/dist/locales.js +8 -0
  54. package/dist/meta-tracking.d.ts +34 -0
  55. package/dist/meta-tracking.js +72 -0
  56. package/dist/middleware.d.ts +100 -0
  57. package/dist/middleware.js +72 -0
  58. package/dist/offer-view.d.ts +10 -0
  59. package/dist/offer-view.js +13 -0
  60. package/dist/otp.d.ts +80 -0
  61. package/dist/otp.js +295 -0
  62. package/dist/phone.d.ts +2 -0
  63. package/dist/phone.js +34 -0
  64. package/dist/product-view.d.ts +12 -0
  65. package/dist/product-view.js +42 -0
  66. package/dist/routes.d.ts +45 -0
  67. package/dist/routes.js +59 -0
  68. package/dist/schemas.d.ts +753 -0
  69. package/dist/schemas.js +377 -0
  70. package/dist/server.d.ts +4 -0
  71. package/dist/server.js +2 -0
  72. package/dist/types.d.ts +418 -0
  73. package/dist/types.js +1 -0
  74. package/dist/utils/image.d.ts +140 -0
  75. package/dist/utils/image.js +131 -0
  76. package/dist/utils/money.d.ts +39 -0
  77. package/dist/utils/money.js +43 -0
  78. package/dist/utils/product-image.d.ts +37 -0
  79. package/dist/utils/product-image.js +38 -0
  80. package/dist/whatsapp.d.ts +55 -0
  81. package/dist/whatsapp.js +89 -0
  82. package/package.json +76 -4
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Deep Money Formatter
3
+ *
4
+ * One formatter for every price a storefront shows.
5
+ * Handles locale-aware formatting, RTL symbol positioning, and Arabic-Indic digits.
6
+ *
7
+ * Design principle: ONE function that handles all cases. Callers curry if needed.
8
+ */
9
+ export interface MoneyFormatOptions {
10
+ /** Currency symbol: 'دج' | 'DA' | 'DZD' */
11
+ symbol: string;
12
+ /** Locale: 'ar-DZ' | 'fr-FR' | 'en-US' */
13
+ locale: string;
14
+ /** Decimal places (default: 0) */
15
+ decimals?: number;
16
+ }
17
+ /**
18
+ * Format money amount with locale-aware formatting.
19
+ *
20
+ * @param amount - Amount in the platform currency's smallest whole unit (DZD)
21
+ * @param options - Formatting options (symbol, locale, decimals)
22
+ * @returns Formatted money string
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Arabic: 5,000 دج (with Arabic-Indic digits)
27
+ * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
28
+ *
29
+ * // French: DA 5 000,00
30
+ * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
31
+ *
32
+ * // English: DA 5,000.00
33
+ * formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
34
+ *
35
+ * // No decimals: 5,000 دج
36
+ * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
37
+ * ```
38
+ */
39
+ export declare function formatMoney(amount: number, options: MoneyFormatOptions): string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Deep Money Formatter
3
+ *
4
+ * One formatter for every price a storefront shows.
5
+ * Handles locale-aware formatting, RTL symbol positioning, and Arabic-Indic digits.
6
+ *
7
+ * Design principle: ONE function that handles all cases. Callers curry if needed.
8
+ */
9
+ /**
10
+ * Format money amount with locale-aware formatting.
11
+ *
12
+ * @param amount - Amount in the platform currency's smallest whole unit (DZD)
13
+ * @param options - Formatting options (symbol, locale, decimals)
14
+ * @returns Formatted money string
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * // Arabic: 5,000 دج (with Arabic-Indic digits)
19
+ * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ' })
20
+ *
21
+ * // French: DA 5 000,00
22
+ * formatMoney(5000, { symbol: 'DA', locale: 'fr-FR' })
23
+ *
24
+ * // English: DA 5,000.00
25
+ * formatMoney(5000, { symbol: 'DA', locale: 'en-US' })
26
+ *
27
+ * // No decimals: 5,000 دج
28
+ * formatMoney(5000, { symbol: 'دج', locale: 'ar-DZ', decimals: 0 })
29
+ * ```
30
+ */
31
+ export function formatMoney(amount, options) {
32
+ const { symbol, locale, decimals = 0 } = options;
33
+ // Format number with locale-aware formatting
34
+ const formatted = amount.toLocaleString(locale, {
35
+ minimumFractionDigits: decimals,
36
+ maximumFractionDigits: decimals,
37
+ });
38
+ // RTL-aware symbol positioning
39
+ // Arabic: suffix (5,000.00 دج)
40
+ // French/English: prefix (DA 5,000.00)
41
+ const isRTL = locale.startsWith("ar");
42
+ return isRTL ? `${formatted} ${symbol}` : `${symbol} ${formatted}`;
43
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Which image represents a product outside its own page.
3
+ *
4
+ * `coverImage` is nullable in the read models and frequently null: a merchant
5
+ * uploads a gallery and never explicitly picks a cover. Reading that field
6
+ * alone gives a product with four photos and no thumbnail — a cart drawer and a
7
+ * checkout summary showing a name beside an empty grey square, on a storefront
8
+ * whose whole job is selling by photograph.
9
+ *
10
+ * Kept as a function rather than an inline fallback so it is one rule with one
11
+ * test, and so a cart, a checkout summary and a product card cannot disagree
12
+ * about which photo a product "is". Pair it with `getThumbnailImage` /
13
+ * `getCardImage` to turn the answer into a resized URL.
14
+ */
15
+ /** The least an uploaded image must carry to be usable as a source. */
16
+ export interface ProductImageSourceInput {
17
+ coverImage?: {
18
+ src?: string | null;
19
+ } | null;
20
+ images?: Array<{
21
+ src?: string | null;
22
+ }> | null;
23
+ }
24
+ /**
25
+ * The product's representative image URL, or `null` when it has none.
26
+ *
27
+ * Prefers the merchant's chosen cover, falls back to the first usable gallery
28
+ * image, and treats a blank `src` as no image at all — so a caller renders its
29
+ * placeholder exactly when the product genuinely has no photograph.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const src = productImageSource(product); // Product | ProductList
34
+ * const thumbnail = src ? getThumbnailImage(src) : null;
35
+ * ```
36
+ */
37
+ export declare function productImageSource(product: ProductImageSourceInput | null | undefined): string | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Which image represents a product outside its own page.
3
+ *
4
+ * `coverImage` is nullable in the read models and frequently null: a merchant
5
+ * uploads a gallery and never explicitly picks a cover. Reading that field
6
+ * alone gives a product with four photos and no thumbnail — a cart drawer and a
7
+ * checkout summary showing a name beside an empty grey square, on a storefront
8
+ * whose whole job is selling by photograph.
9
+ *
10
+ * Kept as a function rather than an inline fallback so it is one rule with one
11
+ * test, and so a cart, a checkout summary and a product card cannot disagree
12
+ * about which photo a product "is". Pair it with `getThumbnailImage` /
13
+ * `getCardImage` to turn the answer into a resized URL.
14
+ */
15
+ /**
16
+ * The product's representative image URL, or `null` when it has none.
17
+ *
18
+ * Prefers the merchant's chosen cover, falls back to the first usable gallery
19
+ * image, and treats a blank `src` as no image at all — so a caller renders its
20
+ * placeholder exactly when the product genuinely has no photograph.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const src = productImageSource(product); // Product | ProductList
25
+ * const thumbnail = src ? getThumbnailImage(src) : null;
26
+ * ```
27
+ */
28
+ export function productImageSource(product) {
29
+ if (!product)
30
+ return null;
31
+ const candidates = [product.coverImage, ...(product.images ?? [])];
32
+ for (const candidate of candidates) {
33
+ const src = candidate?.src;
34
+ if (typeof src === "string" && src.trim().length > 0)
35
+ return src;
36
+ }
37
+ return null;
38
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * WhatsApp prefill messages — the storefront's message rule.
3
+ *
4
+ * A storefront's WhatsApp launcher hands the shopper to WhatsApp with the
5
+ * message already written. The template belongs to the merchant (the dashboard
6
+ * authors it within the limits below), and the page
7
+ * resolves it: `{store}`, `{product}`, `{url}`, `{order}`.
8
+ *
9
+ * Three rules live here so no theme re-invents them:
10
+ *
11
+ * 1. **Ad-click parameters are stripped from `{url}`.** A shopper arriving from
12
+ * a paid ad should not hand the merchant's WhatsApp a `fbclid`-laden link.
13
+ * 2. **The resolved message is capped at `MAX_RESOLVED_PREFILL_LENGTH`, cut at
14
+ * a word boundary.** Browsers and WhatsApp both truncate silently past a
15
+ * point; a cut mid-word reads as a broken store.
16
+ * 3. **The encoded text is appended the same way everywhere**, so a base href
17
+ * that already carries a query string keeps it.
18
+ */
19
+ /**
20
+ * The hard cap on a resolved prefill message, in characters.
21
+ *
22
+ * The same cap the dashboard's character counter enforces.
23
+ */
24
+ export declare const MAX_RESOLVED_PREFILL_LENGTH = 900;
25
+ /**
26
+ * The placeholders a prefill template may carry, resolved from the page the
27
+ * widget renders on.
28
+ *
29
+ * The same tokens the dashboard offers as insert buttons. A token the dashboard offers and the storefront does not
30
+ * resolve is a merchant seeing `{total}` in a sent message.
31
+ */
32
+ export declare const PREFILL_TOKENS: readonly ["product", "url", "order", "store"];
33
+ export type PrefillToken = (typeof PREFILL_TOKENS)[number];
34
+ /** Removes ad-click parameters from a page URL so `{url}` stays clean. */
35
+ export declare function stripTrackingParams(rawUrl: string): string;
36
+ /** Cuts text to `maxLen`, preferring the last word boundary over a split word. */
37
+ export declare function truncateAtWordBoundary(text: string, maxLen?: number): string;
38
+ export interface ResolvePrefillParams {
39
+ template: string;
40
+ storeName: string;
41
+ productName?: string | null;
42
+ url?: string;
43
+ orderNumber?: string | null;
44
+ }
45
+ /**
46
+ * Resolves a merchant template into the message the shopper sends.
47
+ *
48
+ * `{product}` falls back to the store name (a general template rendered on a
49
+ * page with no product), `{order}` resolves to nothing when there is no order,
50
+ * and an unknown placeholder is left verbatim — the dashboard's counter, not
51
+ * this function, is where a typo gets reported.
52
+ */
53
+ export declare function resolvePrefillText({ template, storeName, productName, url, orderNumber, }: ResolvePrefillParams): string;
54
+ /** Appends the resolved message to a `wa.me` href, encoded and query-safe. */
55
+ export declare function buildWhatsAppHref(baseHref: string, text: string): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * WhatsApp prefill messages — the storefront's message rule.
3
+ *
4
+ * A storefront's WhatsApp launcher hands the shopper to WhatsApp with the
5
+ * message already written. The template belongs to the merchant (the dashboard
6
+ * authors it within the limits below), and the page
7
+ * resolves it: `{store}`, `{product}`, `{url}`, `{order}`.
8
+ *
9
+ * Three rules live here so no theme re-invents them:
10
+ *
11
+ * 1. **Ad-click parameters are stripped from `{url}`.** A shopper arriving from
12
+ * a paid ad should not hand the merchant's WhatsApp a `fbclid`-laden link.
13
+ * 2. **The resolved message is capped at `MAX_RESOLVED_PREFILL_LENGTH`, cut at
14
+ * a word boundary.** Browsers and WhatsApp both truncate silently past a
15
+ * point; a cut mid-word reads as a broken store.
16
+ * 3. **The encoded text is appended the same way everywhere**, so a base href
17
+ * that already carries a query string keeps it.
18
+ */
19
+ /**
20
+ * The hard cap on a resolved prefill message, in characters.
21
+ *
22
+ * The same cap the dashboard's character counter enforces.
23
+ */
24
+ export const MAX_RESOLVED_PREFILL_LENGTH = 900;
25
+ /**
26
+ * The placeholders a prefill template may carry, resolved from the page the
27
+ * widget renders on.
28
+ *
29
+ * The same tokens the dashboard offers as insert buttons. A token the dashboard offers and the storefront does not
30
+ * resolve is a merchant seeing `{total}` in a sent message.
31
+ */
32
+ export const PREFILL_TOKENS = ["product", "url", "order", "store"];
33
+ /** Removes ad-click parameters from a page URL so `{url}` stays clean. */
34
+ export function stripTrackingParams(rawUrl) {
35
+ try {
36
+ const url = new URL(rawUrl);
37
+ const keysToDelete = [];
38
+ url.searchParams.forEach((_, key) => {
39
+ const lower = key.toLowerCase();
40
+ if (lower.startsWith("utm_") || lower.endsWith("clid") || lower === "fbclid") {
41
+ keysToDelete.push(key);
42
+ }
43
+ });
44
+ for (const key of keysToDelete) {
45
+ url.searchParams.delete(key);
46
+ }
47
+ return url.toString();
48
+ }
49
+ catch {
50
+ return rawUrl;
51
+ }
52
+ }
53
+ /** Cuts text to `maxLen`, preferring the last word boundary over a split word. */
54
+ export function truncateAtWordBoundary(text, maxLen = MAX_RESOLVED_PREFILL_LENGTH) {
55
+ if (text.length <= maxLen)
56
+ return text;
57
+ const sliced = text.slice(0, maxLen);
58
+ const lastSpace = sliced.lastIndexOf(" ");
59
+ if (lastSpace > 0) {
60
+ return sliced.slice(0, lastSpace).trimEnd();
61
+ }
62
+ return sliced;
63
+ }
64
+ /**
65
+ * Resolves a merchant template into the message the shopper sends.
66
+ *
67
+ * `{product}` falls back to the store name (a general template rendered on a
68
+ * page with no product), `{order}` resolves to nothing when there is no order,
69
+ * and an unknown placeholder is left verbatim — the dashboard's counter, not
70
+ * this function, is where a typo gets reported.
71
+ */
72
+ export function resolvePrefillText({ template, storeName, productName, url, orderNumber, }) {
73
+ const cleanUrl = url ? stripTrackingParams(url) : "";
74
+ const product = productName && productName.trim() ? productName.trim() : storeName;
75
+ const order = orderNumber ? orderNumber.trim() : "";
76
+ const resolved = template
77
+ .replaceAll("{store}", storeName)
78
+ .replaceAll("{product}", product)
79
+ .replaceAll("{url}", cleanUrl)
80
+ .replaceAll("{order}", order);
81
+ return truncateAtWordBoundary(resolved, MAX_RESOLVED_PREFILL_LENGTH);
82
+ }
83
+ /** Appends the resolved message to a `wa.me` href, encoded and query-safe. */
84
+ export function buildWhatsAppHref(baseHref, text) {
85
+ if (!text)
86
+ return baseHref;
87
+ const separator = baseHref.includes("?") ? "&" : "?";
88
+ return `${baseHref}${separator}text=${encodeURIComponent(text)}`;
89
+ }
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@wocart/storefront",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "Typed, validated SDK for Wocart cash-on-delivery storefronts: catalog reads, basket, checkout, OTP and order tracking, with Astro adapters.",
5
+ "keywords": [
6
+ "wocart",
7
+ "storefront",
8
+ "ecommerce",
9
+ "cash-on-delivery",
10
+ "cod",
11
+ "algeria",
12
+ "astro",
13
+ "sdk",
14
+ "typescript"
15
+ ],
16
+ "license": "Apache-2.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/bighadj22/codflow.git",
20
+ "directory": "wocart-sdk/storefront"
21
+ },
22
+ "homepage": "https://github.com/bighadj22/codflow/tree/main/wocart-sdk/storefront#readme",
23
+ "bugs": "https://github.com/bighadj22/codflow/issues",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./server": {
37
+ "types": "./dist/server.d.ts",
38
+ "default": "./dist/server.js"
39
+ },
40
+ "./astro": {
41
+ "types": "./dist/astro.d.ts",
42
+ "default": "./dist/astro.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "USAGE.md",
50
+ "CHANGELOG.md",
51
+ "LICENSE",
52
+ "NOTICE"
53
+ ],
54
+ "scripts": {
55
+ "build": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
56
+ "build:watch": "tsc --watch --preserveWatchOutput",
57
+ "typecheck": "tsc --noEmit && tsc --project tsconfig.test.json",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "verify:package": "npm run build && node scripts/check-package.mjs && node scripts/verify-package.mjs",
61
+ "verify:live": "npm run build && node scripts/verify-live.mjs",
62
+ "prepack": "npm run build",
63
+ "prepublishOnly": "node scripts/check-package.mjs"
64
+ },
65
+ "engines": {
66
+ "node": ">=22.12.0"
67
+ },
68
+ "peerDependencies": {
69
+ "zod": "^4.4.3"
70
+ },
71
+ "devDependencies": {
72
+ "@types/node": "^26.2.0",
73
+ "fast-check": "^4.6.0",
74
+ "typescript": "^7.0.2",
75
+ "vitest": "^5.0.0",
76
+ "zod": "^4.4.3"
77
+ }
78
+ }