---
name: shoe_customizer_project
description: In-house WooCommerce shoe customizer to replace Kickflip; own repo at projects/madalice (was evolution/shoe_customizer)
metadata: 
  node_type: memory
  type: project
  originSessionId: 8e0e15e0-e46b-400b-b3e0-b0563aed3a37
  modified: 2026-07-25T04:06:04.183Z
---

Building an in-house WooCommerce product customizer for custom-branded sneakers to replace the third-party **Kickflip** (MyCustomizer) iframe on corporatekicks.com.au (Shane's site).

**RELOCATED 2026-07-25 → its own git repo `/config/workspace/projects/madalice/` (branch `main`, fresh history, no remote yet).** It's a standalone WooCommerce plugin, not an Evolution feature, so it was moved out of the evolution repo; the `git rm` was committed on evolution's `shoe-customizer` branch (prior per-commit history stays there, not pushed). All paths below that say `shoe_customizer/…` now live under `projects/madalice/…`. The customizer tree is repo root: `README.md` + `AUDIT.md` + `LOG.md` + `docs/` + `poc/` + `plugin/` + `tests/`. The Playwright harness was consolidated to `tests/sc-addtocart-test.mjs` (the newer of the two divergent copies; the stale in-tree + `browser-tools/` copies are gone). See `AUDIT.md` for the current-state audit and `LOG.md` for the timeline.

**How Kickflip works (reverse-engineered 2026-07-24):** iframe from `corporatekicks.gokickflip.com`, engine from `cdnv2.mycustomizer.com`; rendering = **2D layered PNG compositing** of transparent zone slices at 2560×1440. CK owns all the slices already (studio white photos, one per colourable zone). AF1 has 9 zones: swoosh, toecap, vamp, heel, mudguard, collar, tab, laces, sole.

**Locked decisions:** runtime **multiply-tint** (colour × white slice → realistic dyed leather, colours are data); **multi-shoe from day one** with a future Step-0 "Pick a shoe"; orders stay in **WooCommerce** (design JSON + mockup on line item); a **separate** Evolution plugin will pull Woo orders later (keep design JSON self-describing); fully self-hosted, no iframe. Frontend can be vanilla JS canvas (no-build, matches [[portal_minified_no_build]] ethos).

**Status (2026-07-24):** on branch `shoe-customizer` (off staging, pushed to origin).
- **Phase 0 DONE** — multi-shoe runtime-tint POC at `poc/index.html` (self-contained, file://). 5 real shoes seeded from reverse-engineered slices in `poc/assets/<slug>/` (af1, samba, dunk, advantage, court = 53 PNGs); `poc/shoes.js` is the config; "Pick a shoe" step-0 gallery. Commits `97b685cbb` (AF1) → `0c9f84d2b` (multi-shoe).
- **Phase 1 DONE + DEPLOYED & VERIFIED LIVE on prod corporatekicks.com.au (2026-07-24)** — deployable WooCommerce plugin at `plugin/shoe-customizer/`: dbDelta schema (sc_shoe/zone/zone_layer/swatch/option_group/option/visibility_rule/design), `SC_Repo` data layer, `SC_REST` config endpoint (`/wp-json/shoe-customizer/v1/shoe/<slug>` = Phase-2 contract), no-code `SC_Admin` builder (shoe meta + zones repeater w/ wp.media + palette), `SC_Seed` importer (bundled `seed/assets/` → uploads, builds all 5 shoes). Colour path only; priced options/logo/size-matrix = Phase 1b. Shane installed the zip + ran Import seed shoes; smoke test 4/4 green, all 5 shoes serving, 53 slices resolve HTTP 200, no PHP fatals.
- **Deploy tooling** (`plugin/build.sh` zip pkg, `plugin/smoke-test.sh` read-only gate via SC_NETRC, `plugin/DEPLOY.md`, `.gitignore` dist+netrc). Build → deploy(zip upload; I can't install, Shane does) → seed → verify-over-API loop. Prod WC key in [[corporatekicks_woocommerce_api]] (read-only channel from dev env).

- **Phase 2 DONE (v0.2.0, verified vs LIVE prod config)** — `SC_Frontend` `[shoe_customizer slug="af1"]` shortcode + `assets/customizer.js` (same multiply-tint compositor as POC, now fetches `/shoe-customizer/v1/shoe/<slug>`; multi-widget, swatch UI, running price, reset/surprise/download) + `customizer.css`. Proven by running the real customizer.js against live prod config via Playwright (Dunk, 11/11 zones tinted, 0 JS errors).

- **Phase 3 DONE (v0.3.0, php -l + node --check clean; NOT WP-runtime-tested, deploy pending)** — WooCommerce cart/order integration. New `includes/class-sc-cart.php` (`SC_Cart::init()`, loaded only when `function_exists('WC')`): `sc_add_to_cart` admin-ajax handler nonce-gated via `check_ajax_referer('sc_add_to_cart','nonce')` → **server-side re-validates every selection against the shoe's own zones+palette (rejects arbitrary hex), recomputes price server-side** (base price only; no priced options yet), saves scaled mockup PNG via `wp_upload_bits` (data:image/png guard + 8MB ceiling + PNG magic-byte check) + `SC_Repo::save_design()`, then `WC()->cart->add_to_cart($product_id,...,$data)` keyed `sc` with a per-design hash (distinct designs = distinct lines). Surfacing hooks: `woocommerce_get_item_data` (colour rows), `woocommerce_cart_item_thumbnail` (mockup swap), `woocommerce_before_calculate_totals` (absolute idempotent `set_price` — NO reentry guard, that pattern is for compounding price+delta only), `woocommerce_checkout_create_order_line_item` (visible "Design" summary + hidden `_sc_design_id`/`_sc_shoe`/`_sc_selections`/`_sc_mockup`), `woocommerce_after_order_itemmeta` (admin thumb). Repo gained `shoe_by_product()`+`save_design()`; `config()` now emits `productId`. Frontend: shortcode `product` attr + auto-detect current product via `is_product()`/`get_queried_object_id()` → `data-product`; SCData now carries `nonce`+`ajax`+`addAction`. JS: real AJAX add-to-cart (mockup scaled to 1400px longest edge, redirects to cart on success, `.sc-note` error box); **falls back to Phase-2 JSON-emit demo when productId=0** so a plain page still works. Committed `b16bff556`, pushed. **To go live Shane must: upload dist/shoe-customizer-0.3.0.zip, then set each shoe's WooCommerce product ID in the builder (or put the shortcode on the mapped product's description page).**

- **Phase 3 deployed + working on prod (2026-07-24).** v0.3.0 uploaded; add-to-cart initially failed with "isn't linked to a purchasable product yet" because "build your own" Woo products carry **no base price** → WC `is_purchasable()` returns false. **v0.3.1** relaxed the guard to require published+in-stock (not priced), since apply_price() sets the line price server-side — matches what WC_Cart::add_to_cart itself enforces. Shane confirmed add-to-cart works (also works if a placeholder price is set on the product; our customizer overrides it to the shoe base_price). Dunk product = **id 8928** (`dunk-build-your-own`), auto-detected via is_product()/get_queried_object_id() even though seed shoe product_id=0. **v0.3.2** adds the buy-path guard so the customizer is the ONLY way to buy a shoe product: removes default single add-to-cart form, swaps shop-loop button for a "Customize" link, blocks bare add server-side via `woocommerce_add_to_cart_validation` (our AJAX sets `SC_Cart::$adding` flag to pass). Detection `SC_Cart::product_uses_customizer()` = shoe->product mapping OR `has_shortcode()` on product content (covers auto-detect). Latest zip `dist/shoe-customizer-0.3.2.zip`, pushed `45eb348ac`.

- **Phase 3 FULLY VERIFIED on prod (2026-07-24).** Real customer order #9064 (Dunk, $200) carried mockup + `_sc_design_id`/`_sc_shoe`/`_sc_selections` + correct server price. Caught+fixed a display bug: `sanitize_selections()` returned a wrapped `{selections:[...]}` but display hooks iterated `$sc['selections']` as a flat list → the visible "Design" line always read "All white" (v0.3.3 returns flat list; also cleaned `_sc_selections`). Verified live via Playwright add-to-cart (`shoe_customizer/sc-addtocart-test.mjs`, also in `browser-tools/`): swoosh=Royal+toecap=Red → cart line "Design: Swoosh: Royal, Toe cap: Red", $200, mockup thumb, 0 JS errors. GOTCHA: guest add-to-cart nonce is tied to the WC session cookie, so curl-across-requests gets `-1`; drive a real browser instead. Latest zip `dist/shoe-customizer-0.3.3.zip`. Note: product 8928 has stray empty "Shoes Required/Description/File Upload/Options" fields from another add-on — retire during 1b cutover.

- **Phase 1b CODE-COMPLETE (2026-07-24, v0.4.0; php -l + node --check clean; NOT WP-runtime-tested, deploy + staging verify pending Shane).** All three features in one release. Scope at `docs/PHASE_1B_SCOPE.md`; how-to at `docs/GUIDE_OPTIONS_SIZES_LOGO.md`; changelog `plugin/shoe-customizer/CHANGELOG.md`. **No schema change (sc_db_version stays 1).** Locked decisions honoured: PNG/JPG logos only (no SVG), move+scale only (no rotation), 5MB cap (`SC_MAX_LOGO`), MOQ **2** total pairs (`SC_MOQ`), size scale seeded US 6–13.
  - **WS1 SC_Repo**: `option_groups/options/visibility_rules/size_scale`, `save_option_group` (full-replace opts + single rule), `save_size_scale` (type='size' group sort=900 required=1), `delete_option_group`, helpers `norm_print_area` (`{x,y,w,h}` JSON, canvas coords) + `norm_group_type`. `config()` now emits `optionGroups` (opts by CODE-mapped zone), `sizes`, `moq`, per-zone `printArea`.
  - **WS2 builder** (`views/edit.php`+`class-sc-admin.php`): Options repeater (swatch/radio/select/text, +A$ delta, target zone by code, required, one show-only-when rule), Size-run textarea, print-area drawer (`.sc-set-print` modal draws rect → hidden `zone[i][print_area]`), **Re-sync zones from seed** (`SC_Seed::resync` — code-matched zone upsert preserving id+print_area; refreshes base+layers only, never price/palette/options).
  - **WS3 `class-sc-upload.php`**: `sc_upload_logo` admin-ajax (guest-ok), nonce + 5MB + getimagesize + PNG/JPG allowlist, **re-encodes via wp_get_image_editor** (kills polyglots), randomised `logo-<pw>.ext` under `uploads/shoe-customizer/logos/`; returns `{url,w,h}`.
  - **WS4 frontend** (`customizer.js`): renders option groups w/ live delta price + required + fixpoint visibility; size qty-grid w/ live pairs total + MOQ gate; logo upload → **live drag-to-move + corner-handle/slider scale, clamped to printArea**. Editing chrome (dashed area + handle) drawn on a **separate `.sc-overlay` canvas** so it never bakes into the mockup. `design()` adds `options[]`/`sizes[]`/`logo{}`; qty = size total.
  - **WS5 `SC_Cart`**: `sanitize_options` (real option ids only, one/group, visibility fixpoint, required check → error), `sanitize_sizes` (valid codes, total), `sanitize_logo` (URL must be in our logos dir, clamp to print area), `price()` = base + Σ DB deltas (**server re-priced, client never trusted**), qty = server size total (≥MOQ). Persists `_sc_options`/`_sc_sizes`/`_sc_logo` + shows rows in cart/order.
  - **WS6**: docs page + changelog + README + built `dist/shoe-customizer-0.4.0.zip`; Playwright harness `sc-addtocart-test.mjs` extended (option delta / size total / logo upload asserts) — **not run** (needs staging w/ 0.4.0 + a configured shoe; the harness does a live-cart write).
  - **OPEN**: Shane to deploy the zip + configure options/sizes/print-areas per shoe + run staging verify; retire stray "Shoes Required/Options" add-on fields on product **8928** at cutover. Not yet committed/pushed (was on branch `shoe-customizer`).

- **Phase 1b deployed (v0.4.0) + hotfix (v0.4.1, 2026-07-25).** Shane deployed 0.4.0 to prod; hit two bugs. (1) **Save bug — FIXED in 0.4.1:** in `views/edit.php` the "Re-sync zones" `<form>` was nested inside the main Save `<form>`. HTML forbids nested forms → browser reassigns the inner `action=sc_resync_zones` hidden input to the outer form → clicking "Save shoe" fired the resync handler (tell-tale `sc_msg=resynced`), so options/size-run never saved. Un-nested it: resync form is now a sibling, button wired via HTML5 `form="sc-resync-form"`. Save *handler* was always correct. Committed+pushed `205888be9`; zip `dist/shoe-customizer-0.4.1.zip`. (2) **"Critical error adding to cart" — OPEN, needs trace:** audited the whole server add path against live Dunk config (optionGroups/sizes empty, all printArea null) — every branch is empty-config-safe, all 8 cart hook callbacks exist, frontend PHP clean; no fatal reproduces by inspection → environmental. corporatekicks log NOT mirrored to my env (error.log is Evolution ERP only). Shane chose: **deploy 0.4.1, retest**; if it recurs he pastes `wp-content/debug.log` fatal. Plausibly tied to the broken empty-config state.
- **Recolor Q answered:** paid Swatch groups require one **+ Add choice** row per colour (own label/price/zone/hex) — no base-palette-inherit shortcut yet.
- **Phase 1c prototype (POC-first, 2026-07-25):** Shane asked for two new paid-option-group types: (1) **base-palette flat-price** (recolour a zone from the base palette at one flat +A$, White clears) and (2) **HTML colour picker** (any hex, one flat +A$). Decisions: **paid option group ONLY** (free base path unchanged), colour picker **replaces** swatches within a picker group, and **prototype in the POC first, then port**. Done in POC (`poc/shoes.js` + `poc/index.html`, committed `8bf6ba627`): `options:[{id,label,type:'palette'|'colorpicker',zone,price,default?}]`; palette shows base swatches, picker uses native `<input type=color>` gated by an enable checkbox so pricing is unambiguous; both override the free zone selection while active; unit price rolls up live; design JSON carries chosen colour+hex+priceDelta. Sample groups seeded on dunk (lace palette +$15, swoosh picker +$25) + af1. Playwright-verified: 200→215→240, 0 JS errors. **PENDING PORT to plugin** once Shane's happy with UX: new group types in norm_group_type + builder editors (edit.php/admin.js) + config() emit + customizer.js render + **server sanitize_options must validate palette-hex-in-palette and scope arbitrary-hex acceptance to colorpicker zones only** + cart/order display + docs/changelog/build. Note: free zone swatches still show for a zone that also has a paid group (Shane may want to hide the free swatch when a paid group targets that zone — open UX tweak).

Next: await Shane's 0.4.1 deploy + retest (save + cart). If cart fatal recurs, get trace. Then staging verify harness; Phase 4 spec sheet. WC key [[corporatekicks_woocommerce_api]] (read-only from dev). Kickflip admin in [[shoe_customizer_kickflip_admin]]. Tooling: `browser-tools/` Playwright ([[browser_screenshot_tooling]]).
