@feeef.dev/cli 0.2.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 (61) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +94 -0
  3. package/dist/bin.js +2752 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.js +2730 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +67 -0
  9. package/scaffolds/blank/.cursor/rules/feeef-theme.mdc +34 -0
  10. package/scaffolds/blank/.cursor/rules/filterator.mdc +13 -0
  11. package/scaffolds/blank/.cursor/rules/order-form.mdc +17 -0
  12. package/scaffolds/blank/.cursor/rules/theme-source.mdc +32 -0
  13. package/scaffolds/blank/.cursor/skills/feeef-filterator/SKILL.md +15 -0
  14. package/scaffolds/blank/.cursor/skills/feeef-theme/SKILL.md +56 -0
  15. package/scaffolds/blank/.cursor/skills/feeef-theme/reference.md +73 -0
  16. package/scaffolds/blank/.vscode/extensions.json +3 -0
  17. package/scaffolds/blank/.vscode/settings.json +13 -0
  18. package/scaffolds/blank/AGENTS.md +104 -0
  19. package/scaffolds/blank/README.md +21 -0
  20. package/scaffolds/blank/docs/00-INDEX.md +33 -0
  21. package/scaffolds/blank/docs/03-CUSTOM-COMPONENTS.md +116 -0
  22. package/scaffolds/blank/docs/04-WORKFLOW.md +129 -0
  23. package/scaffolds/blank/docs/05-ANTI-PATTERNS.md +80 -0
  24. package/scaffolds/blank/docs/07-SHARED-COMPONENTS.md +144 -0
  25. package/scaffolds/blank/docs/08-ORDER-FORM.md +376 -0
  26. package/scaffolds/blank/docs/09-THEME-PORTING.md +211 -0
  27. package/scaffolds/blank/docs/10-SCHEMA-LIBRARY.md +127 -0
  28. package/scaffolds/blank/docs/11-PAGES-CHECKOUT-THANKS-PRODUCT.md +81 -0
  29. package/scaffolds/blank/docs/12-DARK-LIGHT-THEME.md +164 -0
  30. package/scaffolds/blank/docs/13-TEMPLATE-I18N.md +320 -0
  31. package/scaffolds/blank/docs/14-FILTERATOR.md +433 -0
  32. package/scaffolds/blank/docs/16-AUTHORING-TS-IMPORTS.md +73 -0
  33. package/scaffolds/blank/feeef.template.json +7 -0
  34. package/scaffolds/blank/locales/README.md +4 -0
  35. package/scaffolds/blank/locales/ar.json +10 -0
  36. package/scaffolds/blank/locales/en.json +10 -0
  37. package/scaffolds/blank/package-lock.json +1975 -0
  38. package/scaffolds/blank/package.json +23 -0
  39. package/scaffolds/blank/pages/home/components/hero.tsx +34 -0
  40. package/scaffolds/blank/pages/home/page.json +3 -0
  41. package/scaffolds/blank/props.json +6 -0
  42. package/scaffolds/blank/schema.ts +49 -0
  43. package/scaffolds/blank/tsconfig.json +24 -0
  44. package/scaffolds/blank/types/feeef-live-scope.d.ts +162 -0
  45. package/scaffolds/blank/types/feeef-template-schema.d.ts +84 -0
  46. package/vendor/template-kit/README.md +64 -0
  47. package/vendor/template-kit/bin/dev.mjs +61 -0
  48. package/vendor/template-kit/bin/feeef-template.mjs +212 -0
  49. package/vendor/template-kit/data/lithium-schema.json +4733 -0
  50. package/vendor/template-kit/lib/build.mjs +377 -0
  51. package/vendor/template-kit/lib/compile-component.mjs +188 -0
  52. package/vendor/template-kit/lib/component-meta.mjs +481 -0
  53. package/vendor/template-kit/lib/library.mjs +168 -0
  54. package/vendor/template-kit/lib/locales.mjs +68 -0
  55. package/vendor/template-kit/lib/paths.mjs +84 -0
  56. package/vendor/template-kit/lib/shared.mjs +268 -0
  57. package/vendor/template-kit/lib/theme-schema.mjs +300 -0
  58. package/vendor/template-kit/lib/unpack.mjs +324 -0
  59. package/vendor/template-kit/lib/validate.mjs +207 -0
  60. package/vendor/template-kit/schemas/component.schema.json +55 -0
  61. package/vendor/template-kit/schemas/template.schema.json +18 -0
@@ -0,0 +1,80 @@
1
+ # Anti-Patterns — Do Not Do These
2
+
3
+ > Theme package copy — paths assume this theme folder is the project root.
4
+
5
+ High-signal mistakes AIs and humans make in this system.
6
+
7
+ ## Data shape
8
+
9
+ | Anti-pattern | Correct |
10
+ |---|---|
11
+ | `{ props: { code, propsSchema, slots } }` | Top-level `code`, `propsSchema`, `slots`, `slotsSchema`, `slotsLayout` |
12
+ | Omitting `props` entirely | Always include `props: {}` at minimum |
13
+ | Using `children` on `custom` for composition | Use `slots` + `slotsSchema` |
14
+ | Using `slots` on `grid` | Use `children` |
15
+ | Inventing `slotsLayout` leaf ids not in `slotsSchema` | Leaves must be slot keys |
16
+ | Regenerating `instanceId` on every edit | Keep stable ids |
17
+ | Deleting empty section keys | Keep `components: []` |
18
+
19
+ ## Custom JSX
20
+
21
+ | Anti-pattern | Correct |
22
+ |---|---|
23
+ | `import React from "react"` | Scope already has `React` |
24
+ | `export default function …` | `function App() { … }` only |
25
+ | Bare `useState()` in new code | `React.useState()` |
26
+ | `color: store.decoration.primary` | `hsl(var(--primary))` |
27
+ | Hardcoded light-only `#fff` / `#111` chrome | `hsl(var(--background))` / `hsl(var(--foreground))` — see `docs/12-DARK-LIGHT-THEME.md` |
28
+ | Custom “dark palette” hex map beside Lithium | Use `:root` / `.dark` tokens + `useTheme()` |
29
+ | Hardcoded `'Checkout'` / Arabic copy in custom JSX | `t('checkout.title')` + `docs/13-TEMPLATE-I18N.md` |
30
+ | Theme strings only outside `locales/*.json` | Use theme `locales/*.json` → `data.i18n` |
31
+ | Client-filter entire catalog for stock/price | `in_stock` / `price_min` / filterator — `docs/14-FILTERATOR.md` |
32
+ | `product.media[0].url` | `product.media[0]` (string URL) |
33
+ | Invent related-products API | Same `category_id` / `random` / `sold` |
34
+ | `product.media.map(m => m.url)` | `product.media` is `string[]` |
35
+ | `<a href="/p/x">` for in-app nav | `<Link href="/p/x">` or `RouterNav` |
36
+ | `useSlotContext('x').routerNav.push` | Real `useRouter` / `Link` / `RouterNav` |
37
+ | Assuming `useStore()` non-null | Always null-check |
38
+ | TypeScript / JSX types in published `code` | Author `component.tsx`; build strips types into `code` |
39
+ | Assuming raw `import` works inside react-live without build | Use theme `npm run build` — imports are bundled into `code` |
40
+ | Bundling a second `react` into `code` | Mark react as external (build does this); use live scope |
41
+
42
+ ## Kit / tooling
43
+
44
+ | Anti-pattern | Correct |
45
+ |---|---|
46
+ | Hand-edit `dist/data.json` as primary workflow | Edit `pages/` / `shared/`, then `npm run build` |
47
+ | Use Node APIs inside component source | Use `utils/get-template.ts` |
48
+ | Arbitrary filesystem preview paths | Only `feeef template dev` / CLI override |
49
+ | Expect Shopify reference dumps to render | They are not Feeef themes |
50
+ | Skip `npm run check` after edits | Rebuild and hard-refresh preview |
51
+ | Commit `dist/` as source | Source is the folder tree |
52
+ | Duplicate the same footer/navbar JSX on every page | Promote to `shared/components/<id>/` + `$ref` |
53
+ | Copy dawn-order-form as “complete” checkout | Follow `docs/08-ORDER-FORM.md` + native `order-form.tsx` |
54
+ | `ff.orders.send` from custom JSX | `fetch("/api/orders/create")` |
55
+ | Free-text wilaya/city without cascade/indices | Legacy 1-based indices or new state codes + city names |
56
+ | `useEffect(..., [cartCtx])` that calls `setShippingAddress({ state: null })` | Bootstrap **once** per `product.id`; depend on `cartReady` boolean; preserve existing state/city ([09](./09-THEME-PORTING.md)) |
57
+ | Fake add-to-cart via `updateCurrentItem({})` + click Cart | `cart.addCurrentItemToCart()` (+ `dawn:open-cart` event) |
58
+ | Search with only `params.q` | Prefer `params.search` (Feeef list); URL can stay `?q=` |
59
+ | `product.media.map(m => m.url)` for hover | `media` is `string[]`; dedupe with `photoUrl` |
60
+ | Floating label + `<option>State</option>` on empty select | Empty hidden option + always-floated select label |
61
+ | Fake newsletter subscribe in footer | Store bio / About (`store.description`) |
62
+ | Duplicate header/footer JSX on every page shell | `shared/` + `$ref` from day one |
63
+ | Theme chrome without `dir="ltr"` on RTL store while matching LTR Dawn | Multilang: `dir` from `useFeeefLocale()` + `t()` ([13](./13-TEMPLATE-I18N.md)); force LTR only for LTR-only craft demos |
64
+
65
+ ## Registry / built-ins
66
+
67
+ Prefer built-in types when they fit; otherwise `type: "custom"` with `component.jsx` / `.tsx`.
68
+
69
+ ## AI generation
70
+
71
+ | Anti-pattern | Correct |
72
+ |---|---|
73
+ | Returning slots nested under `props` | Top-level siblings of `code` |
74
+ | Empty `slots: {}` wiping existing children on edit | Omit slots fields to mean “keep existing” (backend contract) |
75
+ | Placeholder `_customComponentDescription` left in production data | Resolve to real `code` before ship |
76
+ | Arabic/RTL ignored for DZ merchants | Respect store language / RTL CSS |
77
+
78
+ ## Production
79
+
80
+ Do not ship local-preview-only overrides to production hosts.
@@ -0,0 +1,144 @@
1
+ # Shared Components — Design Consistency
2
+
3
+ > Theme package copy — paths assume this theme folder is the project root.
4
+
5
+ Feeef has **two** reuse layers. Use the right one.
6
+
7
+ ```mermaid
8
+ flowchart TB
9
+ subgraph authoring [Theme authoring - template-kit]
10
+ sharedDisk["shared/components/footer"]
11
+ placement["pages/.../component.json\n$ref: shared.footer"]
12
+ build["template:build"]
13
+ dataJson["dist/data.json\nfully inlined components"]
14
+ sharedDisk --> build
15
+ placement --> build
16
+ build --> dataJson
17
+ end
18
+
19
+ subgraph runtime [Merchant runtime - Lithium]
20
+ lib["Backend template_components\nlibrary"]
21
+ refNode["type: reference\nrefId + props overrides"]
22
+ resolve["materializeTemplateReferences"]
23
+ custom["type: custom in memory"]
24
+ lib --> resolve
25
+ refNode --> resolve
26
+ resolve --> custom
27
+ end
28
+
29
+ dataJson -->|"install / save to store"| storeMeta["store.metadata.templateData"]
30
+ storeMeta --> resolve
31
+ ```
32
+
33
+ ## 1. Theme shared library (compile-time) — **prefer this for kit themes**
34
+
35
+ **Path:** `templates/<theme>/shared/components/<id>/`
36
+
37
+ Same shape as any component (`component.json` + optional `component.jsx` + slots/children).
38
+
39
+ **Placement** (thin stub — do not copy JSX):
40
+
41
+ ```json
42
+ {
43
+ "order": 0,
44
+ "$ref": "shared.footer",
45
+ "instanceId": "contact_footer_1",
46
+ "title": null,
47
+ "props": {
48
+ "social_media": [{ "platform": "instagram", "url": "https://instagram.com/feeef.dz" }]
49
+ }
50
+ }
51
+ ```
52
+
53
+ Shorthand: `"shared": "footer"` instead of `"$ref": "shared.footer"`.
54
+
55
+ ### What build does
56
+
57
+ 1. Loads every `shared/components/<id>/` into a catalog.
58
+ 2. Expands each placement: clone shared definition → deep-merge `props` → keep placement `instanceId` / `title` / slot overrides.
59
+ 3. Writes a normal `TemplateData` blob (**no `$ref` left** in `dist/data.json`).
60
+
61
+ Lithium never needs to know about `$ref` — the storefront only sees full components.
62
+
63
+ ### Why this exists
64
+
65
+ - **One place to change design** (footer columns, navbar nav items, Dawn chrome) → every page updates on rebuild.
66
+ - Git-friendly: edit `shared/footer/component.jsx` once.
67
+ - Matches `defaults.json`’s historical `"$ref": "shared.navbar"` idea, but as real folders.
68
+
69
+ ### Dawn theme shared ids (current)
70
+
71
+ | Id | Role |
72
+ |---|---|
73
+ | `dawn-footer` | Custom Dawn footer (JSX) |
74
+ | `dawn-header-bar` | Custom Dawn header bar (JSX) — chrome only, not a page shell |
75
+ | `dawn-storefront-shell` | Thin home/products page shell (`header` \| `sections` \| `footer`) |
76
+ | `dawn-thanks-shell` | Thin `/thanks` page shell (`header` \| `body` \| `footer`) |
77
+ | `dawn-order-form` | Legacy single-file COD (prefer field kit below) |
78
+ | `dawn-order-offers` | Offer list tiles (`price × minQuantity`) + per-slot variant loop (`" \| "`) |
79
+ | `dawn-order-qty` | Order form quantity |
80
+ | `dawn-order-name` | Order form name (+ section title) |
81
+ | `dawn-order-phone` | Order form phone (drives draft) |
82
+ | `dawn-order-email` | Order form email |
83
+ | `dawn-order-geo` | State + city row (slots: `stateSlot`, `citySlot`) |
84
+ | `dawn-order-state` / `dawn-order-city` | Wilaya / commune selects |
85
+ | `dawn-order-shipping` | Delivery type cards |
86
+ | `dawn-order-address` / `dawn-order-note` | Street + customer note |
87
+ | `dawn-order-actions` | Add to cart + Buy (place **last** in form content) |
88
+ | `dawn-order-success` | Post-order success UI |
89
+
90
+ **Also ship matching entries under `library/components/`** for editor drag-drop (docs/10). Keep library JSX in sync with `shared/` — library is copy-on-drop, not a live `$ref`.
91
+
92
+ Order-form fields are **shared + slot-ordered** on the PDP form manager — hide via `props.hide`, reorder by changing `order` / folder siblings, add/remove by adding/removing `$ref` placements. Behavior contract: [`08-ORDER-FORM.md`](./08-ORDER-FORM.md).
93
+
94
+ **Rule of thumb:** if the same UI appears on 2+ pages, it **must** live under `shared/` and be referenced — do not paste duplicate `component.jsx` / props.
95
+
96
+ ## 2. Backend library references (runtime)
97
+
98
+ ```json
99
+ {
100
+ "type": "reference",
101
+ "refId": "cuid_of_template_components_row",
102
+ "refVersion": 3,
103
+ "instanceId": "hero_on_home",
104
+ "props": { "title": "Override" },
105
+ "slots": {}
106
+ }
107
+ ```
108
+
109
+ - Source of truth: per-store (or public) rows in `template_components`.
110
+ - Resolved in `getTemplate()` by `materializeTemplateReferences()` → becomes `type: "custom"`.
111
+ - Merchants reuse library blocks across stores; editing the library updates all refs (subject to versioning/policy).
112
+
113
+ **Do not confuse** `refId` (backend) with `$ref: "shared.*"` (disk kit). Different planes.
114
+
115
+ | | Kit `$ref` / `shared` | Runtime `type: "reference"` |
116
+ |---|---|---|
117
+ | When resolved | `template:build` | SSR `getTemplate` |
118
+ | Storage | Folders on disk | DB `template_components` |
119
+ | Audience | Theme engineers | Merchants / marketplace |
120
+ | In `data.json` | Expanded away | Kept until SSR |
121
+
122
+ ## 3. Defaults.json `$ref` (legacy blank store)
123
+
124
+ `(platform)` still has a top-level `"shared"` map and `"$ref": "shared.navbar"` inside page trees. New themes should use disk `shared/components` instead of a JSON shared map.
125
+
126
+ ## 4. Consistency checklist
127
+
128
+ When adding UI to a theme:
129
+
130
+ 1. Search `shared/components/` — reuse if it exists.
131
+ 2. If copying a block to a second page → **promote** it to `shared/components/<id>/` first, then `$ref` from both places.
132
+ 3. Page-specific differences → **props overrides only** on the placement (URLs, labels), not a fork of the JSX.
133
+ 4. Global look (colors, corners, fonts) → root `props.json` / CSS variables — not hardcoded in every custom component.
134
+ 5. After edits: `npm run build` and spot-check every page that refs the shared id.
135
+
136
+ ## 5. Anti-patterns
137
+
138
+ | Bad | Good |
139
+ |---|---|
140
+ | Copy-paste `component.jsx` onto home + products | One `shared/x` + two `$ref` stubs |
141
+ | Fork shared by duplicating folder “footer2” | Override `props` on the placement |
142
+ | Leave `$ref` in published `data.json` manually | Always run `template:build` |
143
+ | Use `type: "reference"` for local theme files | Kit `$ref` for themes; `reference` for DB library |
144
+ | Change shared + forget rebuild | `template:watch` / `template:dev` |
@@ -0,0 +1,376 @@
1
+ # Order Form Contract — AI Must Follow
2
+
3
+ > Theme package copy — paths assume this theme folder is the project root.
4
+
5
+ Canonical native implementation:
6
+
7
+ - the native Lithium `OrderForm` — production `OrderForm`
8
+ - `POST /api/orders/create` — **required** submit proxy
9
+ - Types/config: order field types
10
+ - Security: storefront order-create security checks
11
+ - Scope API: `docs/03-CUSTOM-COMPONENTS.md`
12
+
13
+ When writing a **custom** order form (`type: "custom"` / `component.jsx`), you must reproduce this contract. Styling may change; behavior must not.
14
+
15
+ The shared Dawn form (`shared/components/dawn-order-form/`) is a **simplified visual reference** — it is **incomplete** vs native (no draft, no wilaya cascade, no security, calls `ff.orders.send` directly). Do **not** copy it as the behavioral source of truth.
16
+
17
+ ---
18
+
19
+ ## 0. Hard rules (fail the task if violated)
20
+
21
+ 1. **Submit via `POST /api/orders/create`** — never call `ff.orders.send()` from the browser in production custom forms (loses real IP / UA forwarding).
22
+ 2. **Draft + pending** — send `status: "draft"` when phone reaches exact length; send `status: "pending"` on final submit; reuse draft `id` on pending.
23
+ 3. **Legacy vs new geo** — branch on `store.configs.selectedCountry` (see §3). Wrong encoding breaks shipping + backend validation.
24
+ 4. **Security on pending only** — honeypot, min 3s dwell, in-flight lock, fake id prefix `FuHe3nf`, pixel suppression.
25
+ 5. **Cart bootstrap** — set current item + shipping method/address before reading shipping types/prices.
26
+ 6. **`React.use*` hooks** — no imports; null-check `useFeeefCart` / `useStore` / `useFeeef`.
27
+ 7. Prefer native registry `product_order_form` when design allows; custom only when UI must diverge.
28
+
29
+ ---
30
+
31
+ ## 1. Lifecycle
32
+
33
+ ```
34
+ mount
35
+ → formLoadTime = Date.now()
36
+ → optional device fingerprint in metadata (if your form collects one)
37
+ → cart.setCurrentItem({ product, quantity })
38
+ → cart.setShippingMethod(product|store)
39
+ → load states (legacy utils OR feeef.states.list)
40
+
41
+ phone digits length === getPhoneLength(country)
42
+ → if sendDrafts !== false AND no draft yet AND not sending
43
+ → POST /api/orders/create { status: "draft", id?: undefined, ... }
44
+ → keep draft.id in ref+state
45
+ → optional Lead / Purchase pixel (skip if fake)
46
+
47
+ user changes city (and draft exists)
48
+ → optional draft update (same id, status draft)
49
+
50
+ submit
51
+ → client validateRequiredFields
52
+ → security checks (pending only)
53
+ → POST /api/orders/create { status: "pending", id: draft?.id, ... }
54
+ → cart.clear()
55
+ → pixels (unless suppressed)
56
+ → success UI / redirect / payment step
57
+ ```
58
+
59
+ ### Phone length
60
+
61
+ ```ts
62
+ const COUNTRY_PHONE_LENGTHS = { DZ: 10, TN: 10, IQ: 11, LY: 10 };
63
+ // default 10
64
+ ```
65
+
66
+ Draft fires when `phone.replace(/\D/g, "").length === requiredLength` — **not** on blur alone.
67
+
68
+ Use a **ref** (`draftOrderRef`, `draftOrderSendingRef`) so rapid keypresses cannot double-send drafts. State alone is too slow.
69
+
70
+ ---
71
+
72
+ ## 2. API — `POST /api/orders/create`
73
+
74
+ ### Why the proxy exists
75
+
76
+ [`route.ts`](../../app/api/orders/create/route.ts) sets:
77
+
78
+ - `x-feeef-real-ip` from `x-forwarded-for` / `x-real-ip` / `cf-connecting-ip`
79
+ - `x-feeef-client-user-agent` from the browser UA
80
+ - Geo hint into `metadata.debug.country` (`x-vercel-ip-country`)
81
+
82
+ Then calls server SDK `ff.orders.send(data)`.
83
+
84
+ ### Request body (minimum production shape)
85
+
86
+ ```ts
87
+ {
88
+ id?: string, // draft id when upgrading draft → pending
89
+ storeId: string,
90
+ customerPhone: string, // required
91
+ customerName?: string,
92
+ customerEmail?: string,
93
+ shippingCountry?: string, // UPPERCASE ISO, e.g. "DZ"
94
+ shippingState?: string, // legacy: "1".."58" | new: state code
95
+ shippingCity?: string, // legacy: "1"..N index | new: city name
96
+ shippingAddress?: string,
97
+ shippingType: "home" | "pickup" | "store",
98
+ customerNote?: string,
99
+ status: "draft" | "pending",
100
+ source?: string, // traffic source detection
101
+ references?: string[], // session attribution tokens
102
+ customFields?: Record<string, any>,
103
+ items: Array<{
104
+ productId: string,
105
+ quantity: number,
106
+ variantPath?: string, // "Color/Red" OR expand "A | B" → two items qty 1
107
+ offerCode?: string,
108
+ addons?: Record<string, number>, // title → qty
109
+ }>,
110
+ metadata: {
111
+ metaPixel?: { fbclid?, fbc?, fbp?, eventSourceUrl? },
112
+ deviceFingerprint?: string,
113
+ formLoadTime: number, // ms epoch from mount
114
+ platform: "web;lithium v1.0",
115
+ email?: string,
116
+ clarity?: { userId: string },
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### Response handling
122
+
123
+ | Status | Behavior |
124
+ |---|---|
125
+ | 200 | Order entity (`id`, totals, shipping…); treat as success |
126
+ | 422 | Field errors `{ errors: [{ field, message }] }` — map `customFields.<id>` |
127
+ | 403 | Security / ads-only — show i18n-friendly message |
128
+ | 429 | Rate limit — “too many orders” |
129
+
130
+ ### Fake / bot responses
131
+
132
+ Any order `id` starting with **`FuHe3nf`** is synthetic. **Do not** fire pixels, clear cart for “success” analytics, or treat as a real order for tracking links.
133
+
134
+ Helper: `shouldSuppressOrderPixelEvents(order)` in `order-create security checksorder-pixel-suppression.ts`.
135
+
136
+ ---
137
+
138
+ ## 3. Country / state / city (the hard part)
139
+
140
+ ### Decide mode
141
+
142
+ ```ts
143
+ const isLegacy = !store?.configs?.selectedCountry;
144
+ ```
145
+
146
+ ### Legacy Algeria (no `selectedCountry`)
147
+
148
+ | Field | UI | Stored value sent to API |
149
+ |---|---|---|
150
+ | State (wilaya) | Labels from `utils/states.ts` via `getStates(shippingRates)` as `"01 - أدرار"` | **1-based index string** `"1"`…`"58"` |
151
+ | City (commune) | Names from `getCitiesByWilayaCode(stateCode)` | **1-based index string** of the commune list |
152
+
153
+ If the UI holds a **city name**, convert to index before submit:
154
+
155
+ ```ts
156
+ const cities = getCitiesByWilayaCode(state);
157
+ const idx = cities.findIndex((c) => c === cityName);
158
+ shippingCity = idx !== -1 ? String(idx + 1) : shippingCity;
159
+ ```
160
+
161
+ Filter wilayas whose shipping rate is `[null, null]` out of the select.
162
+
163
+ Use Feeef states/cities APIs or legacy cascade helpers from the storefront scope.
164
+
165
+ ### New multi-country (`selectedCountry` set)
166
+
167
+ | Field | Source | Stored value |
168
+ |---|---|---|
169
+ | Country | `store.configs.selectedCountry` | Uppercase ISO in `shippingCountry` |
170
+ | State | `feeef.states.list({ countryCode })` filtered by shipping prices | **State code** |
171
+ | City | `feeef.cities.list({ countryCode, stateCode })` on state change | **City name** (localized via `entity.locales?.[lang]`) |
172
+
173
+ Show loading while cities fetch. Clear city when state changes.
174
+
175
+ ### Shipping types & cart
176
+
177
+ ```ts
178
+ cart.setShippingMethod(product /* or store */);
179
+ cart.updateShippingAddress({ country, state, city, type });
180
+ cart.getAvailableShippingTypes(); // home | pickup | store with prices
181
+ cart.getShippingPriceForType(type);
182
+ ```
183
+
184
+ Terminology gotcha in rate keys:
185
+
186
+ | UI `ShippingType` | Often keyed as |
187
+ |---|---|
188
+ | `home` | home delivery |
189
+ | `pickup` | stopdesk / desk |
190
+ | `store` | collect at merchant (sometimes rate key `pickup` in price data) |
191
+
192
+ Hide shipping selector when ≤1 available type or `config.shipping.visible === false`.
193
+
194
+ Bootstrap helper: `lib/bootstrap-product-cart-shipping.ts` — use if you skip native `OrderForm`.
195
+
196
+ **Do not re-run bootstrap on every cart update.** `useFeeefCart()` often returns a new `cartCtx` object whenever shipping/address changes. If your effect depends on `cartCtx` and calls `setShippingAddress({ state: null, city: null })`, the form will reset on every keystroke / wilaya change. Pattern: bootstrap **once per `product.id`** (ref + `cartReady` boolean), and **preserve** existing `state` / `city` / `type`. Full write-up: [`09-THEME-PORTING.md`](./09-THEME-PORTING.md) §4.
197
+
198
+ ---
199
+
200
+ ## 4. Client validation (mirror native defaults)
201
+
202
+ Defaults from native parsers:
203
+
204
+ | Field | Visible | Required |
205
+ |---|---|---|
206
+ | phone | yes | yes |
207
+ | name | yes | yes |
208
+ | state | yes | yes |
209
+ | city | yes | yes |
210
+ | address | no | no |
211
+ | email | no | no |
212
+ | notes | no | no |
213
+
214
+ Rules:
215
+
216
+ - Phone digit length must equal `getPhoneLength(country)`.
217
+ - If `fields.phone.validate === true` **and** country is DZ: `validatePhoneNumber(tryFixPhoneNumber(phone))` from `feeef` SDK. Prefer **off** for non-DZ.
218
+ - Email regex if required: `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`.
219
+ - If `product.variant.required` and no `variantPath` → block.
220
+ - If `requireOffer` and offers exist and none selected → block.
221
+ - Scroll / focus first invalid field; toast; do not call API.
222
+
223
+ ---
224
+
225
+ ## 5. Security (pending submits)
226
+
227
+ Implement all of these in custom forms (or call a shared helper if you extract one):
228
+
229
+ | Check | Behavior |
230
+ |---|---|
231
+ | **Honeypot** | Hidden input `name="website"` (off-screen, not `display:none`). If filled → return fake `FuHe3nf…` order, no API. |
232
+ | **Timing** | If `Date.now() - formLoadTime < 3000` on pending → fake success. |
233
+ | **In-flight lock** | `useRef` boolean around save; throw / ignore `ORDER_IN_FLIGHT`. |
234
+ | **doubleSend** | `order-create security checksdoubleSend.ts` — key `storeId + productIds + phone`. |
235
+ | **frontendStorage** | If security integration frontend option active: block / fake / warning per `order-create security checksfrontendStorage.ts`. |
236
+ | **In-flight lock** | Block double-submit while pending request is open. |
237
+ | **Pixels** | Suppress for fake ids / security treatments. Dedupe: if draft already sent Purchase, don’t send again on pending. |
238
+
239
+ Drafts skip most bot checks (still use in-flight locks).
240
+
241
+ ---
242
+
243
+ ## 6. Variants, offers, addons, quantity
244
+
245
+ ### Variants
246
+
247
+ - Path shape: `"Color/Red/Size/L"`.
248
+ - Bundle multi-select: `"Red | Blue"` → **expand** to two `items[]` entries each `quantity: 1` before send (see `expandVariantPaths` in native).
249
+
250
+ ### Offers
251
+
252
+ - `cart.updateCurrentItemOffer(offer)` / `cart.updateItemOffer(item, offer)`.
253
+ - Clamp quantity to `[offer.minQuantity, offer.maxQuantity]`.
254
+ - Send `offerCode: offer.code` on items.
255
+
256
+ ### Addons
257
+
258
+ - Map of addon title → quantity on the cart item.
259
+ - Send as `addons: Record<string, number>` on each item.
260
+
261
+ ### Quantity
262
+
263
+ - Update via `cart.updateCurrentItem({ quantity })`.
264
+ - Respect offer min/max with user feedback.
265
+
266
+ ### Payment
267
+
268
+ - Default COD.
269
+ - If store/product payment integration enabled: after successful pending create, show payment UI instead of immediate thank-you (native: `PaymentMethodSelection`).
270
+
271
+ ---
272
+
273
+ ## 7. Success UX
274
+
275
+ Native options:
276
+
277
+ - Modal `OrderSuccessDialog` + optional `autoRedirect` (~5s → `/`)
278
+ - Track URL: `https://api.feeef.org/track/{orderId}`
279
+ - Thank-you page: `/thanks?order={id}` (optional `name`, `total`, `phone`). Legacy `/thank-you` redirects.
280
+
281
+ Never redirect / track for `FuHe3nf` fake ids.
282
+
283
+ Clear cart only after confirmed real pending success.
284
+
285
+ ---
286
+
287
+ ## 8. Custom JSX checklist (react-live)
288
+
289
+ ```jsx
290
+ function App() {
291
+ const { product } = useCurrentProduct();
292
+ const cartCtx = useFeeefCart();
293
+ const storeCtx = useStore();
294
+ const router = useRouter();
295
+ // NO ff.orders.send in browser for production — use fetch("/api/orders/create")
296
+
297
+ const cart = cartCtx?.cart;
298
+ const store = storeCtx?.store || cartCtx?.store;
299
+
300
+ const formLoadTimeRef = React.useRef(Date.now());
301
+ const draftRef = React.useRef(null);
302
+ const sendingRef = React.useRef(false);
303
+ const [honeypot, setHoneypot] = React.useState("");
304
+
305
+ // … state/city cascade per §3 …
306
+ // … draft on phone length per §1 …
307
+ // … pending via /api/orders/create per §2 …
308
+ }
309
+ ```
310
+
311
+ Must include:
312
+
313
+ - [ ] Null guards for cart/store/product
314
+ - [ ] Cart current item + shipping bootstrap
315
+ - [ ] Legacy/new geo branch
316
+ - [ ] Draft + pending with draft `id`
317
+ - [ ] Honeypot `website` + 3s rule + in-flight ref
318
+ - [ ] Fake id / pixel suppression
319
+ - [ ] `fetch("/api/orders/create")` with JSON body
320
+ - [ ] Expand `" | "` variant paths
321
+ - [ ] CSS via `hsl(var(--primary))` etc.
322
+
323
+ ---
324
+
325
+ ## 9. Prefer built-in when possible
326
+
327
+ Registry types:
328
+
329
+ | Type | Role |
330
+ |---|---|
331
+ | `product_order_form` | Full form |
332
+ | `product_order_form_variants` | Variants only |
333
+ | `product_order_form_offers` | Offers only |
334
+ | `product_order_form_addons` | Addons only |
335
+
336
+ Compose with **slots** around native blocks when you only need custom chrome. Reimplementing the whole form in JSX is last resort.
337
+
338
+ Props for native form: `built-in Lithium registry (not in this theme)` → `components.product_order_form`.
339
+
340
+ ---
341
+
342
+ ## 10. Attribution / analytics extras
343
+
344
+ - `detectTrafficSource()`, `getFbc()`, `getFbp()`, Facebook params — `order-form/utils/source-detection.ts`
345
+ - Order references session: `lib/order-references-session.ts` → `references[]` on pending, clear after success
346
+ - Clarity: `data-clarity-unmask="true"` on phone/name; optional `metadata.clarity.userId`
347
+
348
+ ---
349
+
350
+ ## 11. File map (native)
351
+
352
+ | Path | Why |
353
+ |---|---|
354
+ | `components/feeef/order-form/order-form.tsx` | Source of truth behavior |
355
+ | `components/feeef/order-form-v2/*` | Newer split (submit hook) — keep in sync when reading |
356
+ | `app/api/orders/create/route.ts` | Proxy + IP |
357
+ | `utils/states.ts` / `utils/cities.ts` | Legacy DZ geo |
358
+ | `order-create security checks*` | Double-send, storage, pixel suppress |
359
+ | `lib/bootstrap-product-cart-shipping.ts` | Cart shipping init |
360
+ | `components/forms/cart-form.tsx` | Multi-item cart checkout variant |
361
+
362
+ ---
363
+
364
+ ## 12. Anti-patterns (order forms)
365
+
366
+ | Bad | Good |
367
+ |---|---|
368
+ | `ff.orders.send(...)` in custom JSX | `fetch("/api/orders/create", …)` |
369
+ | Free-text wilaya/city always | Cascade + correct legacy indices / new codes |
370
+ | Draft on every keypress | Exact digit length + sending ref |
371
+ | `useState` only for submit lock | `useRef` in-flight |
372
+ | Fire Purchase on fake `FuHe3nf` | Suppress pixels |
373
+ | One item with `variantPath: "A \| B"` | Expand to two items |
374
+ | Skip shipping bootstrap | `setShippingMethod` + address before prices |
375
+ | Re-bootstrap on `[cartCtx]` and wipe state/city | Once per product; preserve address ([09](./09-THEME-PORTING.md)) |
376
+ | Copy dawn-order-form as complete | Use this doc + native `order-form.tsx` |