# DRESSX Try-On - Shopify App (Reusable Architecture)

Vanilla JS try-on **popup** injected into Shopify PDPs (script-tag, no bundler).
`try-on_shopifyapp_combined_test.js` is a refactor of the monolithic
`try-on_shopifyapp_test.js` into a **reusable, decoupled architecture** built
from **logical modules** and **UI components** (small atoms + view screens).

Its runtime behaviour is **identical** to the original: same HTML output, same
CSS, same `data-*` hooks, same API payloads, same analytics events and the same
screen/step state machine. The refactor changes *how the code is organized*, not
*what it does*.

> This document is meant to be handed to other developers/agents so they can
> understand, reuse, and extend the widget without reading all ~2.4k lines.
> Cursor agents also load short rules from `.cursor/rules/`.

---

## Editing guidelines (source of truth)

| Do | Don't |
|---|---|
| Edit **`try-on_shopifyapp_combined_test.js`** for features and fixes | Treat the monolith as the day-to-day edit target |
| Keep ASCII-only JS (`\uXXXX` for special glyphs) | Introduce ES modules / npm / a bundler for the widget |
| Add UI as pure HTML-string atoms/views + `data-action` | Mutate the DOM or call the network from views |
| Scope CSS under `#__dressx_shopifyapp_widget` | Change API payloads / Amplitude event names casually |
| Use `local_test.html` to verify screens | Assume localhost generation will succeed (API may 403) |

**Parity reference:** `try-on_shopifyapp_test.js` — only touch when intentionally syncing behaviour both ways.

**Agent rules:** `.cursor/rules/` (`project-overview`, `combined-architecture`, `ui-and-events`, `host-integration`).

---

## Quick facts

| | |
|---|---|
| Entry file (edit this) | `try-on_shopifyapp_combined_test.js` |
| Original (parity reference) | `try-on_shopifyapp_test.js` |
| Loader (reference / CDN-style) | `loader_shopifyapp_test.js` (this repo) |
| Loader (live Shopify PDP / ngrok) | `dx-tryon-shopify-app` → `backend/webapp/static/assets/js/loader_shopifyapp_ngrok.js` |
| Local test harness | `local_test.html` + `serve.py` |
| Agent onboarding | `AGENTS.md` + `.cursor/rules/` |
| Sibling Shopify app | `dx-tryon-shopify-app` (theme extension + Django; no `template/` widget copy) |
| Wrapper DOM id | `#__dressx_shopifyapp_widget` |
| Trigger button id | `#__try_on_btn` |
| Remote API | `https://api.dressxagent.com` |
| Encoding | pure ASCII (all special glyphs are `\uXXXX` escapes) |

---

## Architecture at a glance

Everything lives inside a single IIFE. A shared container object `ctx` acts as a
tiny dependency-injection registry: **each module is a factory that receives
`ctx` and resolves its dependencies from it at call time**, so wiring is
order-independent and every piece stays individually reusable.

```text
try-on_shopifyapp_combined_test.js  (one IIFE, shared `ctx` registry)
|
+-- APP SHELL  (ctx.app)
|     render | rerender | bindEvents | open / close
|     (owns the DOM node, routes user actions, renders the active screen)
|
+-- LOGICAL COMPONENTS  (behaviour / data)
|     config .......... reads host globals, feature-gates the widget
|     store(state) .... single state object + reset()
|     api ............. all HTTP calls to the DressX API
|     engine .......... try-on generation flow / orchestration
|     actions ......... handlers for user actions (data-action)
|     camera .......... selfie capture
|     mixmatch ........ catalog / mix & match data
|     sizing .......... size recommendation
|     tracker ......... analytics (amplitude events)
|     utils / assets / constants ... helpers, icons, static strings
|
+-- UI COMPONENTS  (pure functions -> HTML string)
      |
      +-- A. Small components (atoms)
      |     buttons: icon / primary / link / round
      |     checkbox
      |     cards: styleCard / mixMatchCard
      |     inputs: select / search / pickerOption / selectorPill
      |     chip / progressBar
      |
      +-- B. View components (screens)   [composed from atoms above]
            StyleScreen | Upload/Camera | Preview/Selfie
            Generating  | Result        | MixMatch

Data flow:
  HOST product page --(user clicks TRY ON)--> APP SHELL
  APP SHELL --(renders screen by state.step)--> View component
  View component --(composed from)--> Small components (atoms)
  View component --(reads)--> store(state) / mixmatch / sizing
  user action (data-action) --> engine / actions / camera
  engine / actions --(HTTPS)--> DressX API (api.dressxagent.com)
```

### Layered dependency rule

```
Foundation (assets, constants, utils)      <- no dependencies
      |
Integration (config)                       <- reads host globals + Foundation
      |
State (store)  +  Analytics (tracker)
      |
Domain logic (api, mixmatch, sizing,
              camera, engine, actions)     <- state + api + foundation
      |
UI (ui atoms -> views), styles             <- render from state + logic
      |
App shell (app) -> Bootstrap               <- mounts, renders, binds events
```

Dependencies always point **downward**. UI never calls the network directly; it
renders from `store` state and delegates user actions (via `data-action`
attributes) to `engine` / `actions` / `camera` through the app shell.

---

## Module reference (logical components)

Numbers match the section banners in the source file.

### 0. Enable gate
Runs before anything else. Reads `window.DRESSX_TRYON.tryonEnabled` (falling back
to `window.DRESSX_TRYON_ENABLED`, default `true`) and **returns early** if the
widget is disabled, so nothing mounts on the host page.

### 1. `ctx.assets`
Static presentational assets. `svg` (icon markup keyed by name: `back`, `close`,
`camera`, `chevronDown`, `play`, `share`, `sparkle`, `search`, `mmCheck`,
`switchCamera`, `uploadPhoto`) and `styleImages` (`selfie`, `fullBody`).

### 1. `ctx.constants`
Option lists used by pickers: `SIZE_OPTIONS`, `HEIGHT_OPTIONS`, `GENDER_OPTIONS`.

### 2. `ctx.utils` (pure helpers, no side-effects)
`normalizeImageUrl`, `imageUrlFromElement`, `escapeHtml`, `normalizeUkSize`,
`normalizeLetterSize`, `heightToApi`, `genderToApi`, `selectorDisplayValue`,
`createTryonFlowId`, `tryonRawToDownloadableUrl`, `getAvatarIdFromResponse`,
`svgAsset`.

### 3. `ctx.config` (integration contract)
Reads `window.DRESSX_TRYON`, `window.__DRESSX_TRYON_PARAMS` and the product image
element (`#img-0 img`). Exposes product metadata (`product_title`, `price`,
`variant_id`, `sku`, `product_category`, ...), the resolved `imgSrc` /
`imgSrcBack`, `productImageEl`, `source_company` (`'shopifyapp'`), and feature
flags (`features`, `hasSelfie()`, `hasMixMatch()`, `hasFullBody()`, `entryStep()`).
If no image URL can be resolved, the widget aborts early.

### 4. `ctx.store` (single source of truth)
Holds the entire `state` object. Public API: `state`, `reset()`,
`styleValue()`, `currentPickerValue(type)`. `state.step` drives which screen is
shown (see the state machine below).

### 5. `ctx.tracker` (analytics)
`track(eventName, extra)` sends events to `window.amplitude` with a base payload
built from `config` (product + source + signin + bg_generation). Also
`dxSignedIn()`, `isBgGenerationActive()`.

### 6. `ctx.api` (pure network layer)
No state or tracker side-effects; just `fetch` wrappers returning promises:
`askImageSize`, `createTryon`, `createMixMatchTryon`, `getTryonStatus`,
`getMixMatchStatus`, `createAvatar`, `getAvatarStatus`, `createVideo`.

### 7. `ctx.mixmatch` (catalog selection model)
Product catalog logic for "Complete the look": `normalizeProductImageUrl`,
`getProductsByCategory`, `filterProducts`, `findProductById`, `select`,
`prepareSelections`, `applyRandom`, `getSelectedGarmentUrls`,
`getSelectedGarmentRefs`, `getSelectedProducts`.

### 8. `ctx.sizing` (recommended-size helpers)
`displayRecommendedSize(ownerKey)`, `resultSizeOptions(ownerKey)`.

### 9. `ctx.camera` (camera controller)
Wraps `getUserMedia`: `hasStream`, `facingMode`, `error`, `setDefaults`, `stop`,
`cancelCountdown`, `startCountdown`, `attachStream`, `start`, `openScreen`,
`closeScreen`, `capture`.

### 10. `ctx.engine` (generation orchestration)
Coordinates the full generation lifecycle - progress animation, status polling,
avatar flow and video: `clearIntervals`, `startGeneration`,
`startVideoGeneration`, `startMixMatchTryOn`, `onVideoChipClick`.

### 11. `ctx.actions` (commerce + output)
`runTryonDownload`, `addToCart`, `downloadImage`, `saveTryonAction`,
`getTryonImageBlob`.

### 14. `ctx.styles`
A function returning the widget's CSS as a string (copied verbatim from the
original), scoped under `#__dressx_shopifyapp_widget`.

### 15. `ctx.app` (application shell)
Mount + orchestration: `getWrapper`, `render`, `reset`, `open`, `close`,
`rerender`, `rerenderPreservingMixMatchScroll`, `handleFileChange`,
`handleSelectedFile`, `bindEvents`. `bindEvents` uses event delegation on the
wrapper and dispatches by `data-action`.

### 16. Bootstrap
Creates the floating **TRY ON** button (`#__try_on_btn`), places it over the
product image, and opens the widget on click.

---

## UI components

### 12. `ctx.ui` - small atoms (reusable presentational pieces)

Each atom is a pure function returning an HTML string. All are used by the views.

| Atom | Kind | Purpose |
|---|---|---|
| `iconButton` | button | Icon-only button (header back/close). |
| `primaryButton` | button | Solid CTA (Generate, Add to cart, Try on, Shuffle). |
| `linkButton` | button | Underlined text button (Change photo, Generate video...). |
| `roundButton` | button | Circular icon button (share result). |
| `chip` | control | Bottom action chip with icon + title + subtitle (Video, Complete the look). |
| `checkbox` | control | Selectable check indicator used inside mix-and-match cards. |
| `progressBar` | display | Determinate progress bar for the generating screen. |
| `styleCard` | card | Selfie / Full-body choice card. |
| `mixMatchCard` | card | Selectable product card (image + `checkbox`). |
| `selectorPill` | control | Height / Size / Gender pill that opens a picker. |
| `pickerOption` | control | One selectable option row inside a picker sheet. |
| `searchInput` | input | Mix-and-match search box. |
| `select` | input | Native `<select>` for recommended size. |

### 13. `ctx.views` - screen components (composed from atoms)

| View | Screen / role |
|---|---|
| `Header` | Top bar (back + "powered by" + close). |
| `StyleScreen` | Choose Selfie vs Full-body (uses `styleCard`). |
| `UploadScreen` | Upload dropzone + take-photo. |
| `CameraScreen` | Live camera capture. |
| `PreviewScreen` | Full-body preview + generate. |
| `SelfiePreviewScreen` | Selfie preview + height/size/gender selectors. |
| `SelfieSelector` | Thin wrapper over `selectorPill`. |
| `PickerSheet` | Bottom sheet of `pickerOption`s (height/size/gender). |
| `GeneratingScreen` | Loading screen with `progressBar`. |
| `ResultScreen` | Try-on result + share + product card / mix-match drawer. |
| `MixMatchResultProducts` | "You are wearing" list on the result screen. |
| `ResultSizeSelect` | Recommended-size `select` for a product. |
| `MixMatchRow` | One category row (tops/bottoms/shoes) of `mixMatchCard`s. |
| `MixMatchScreen` | Full mix-and-match screen (search + rows + actions). |
| `renderScreen` | Dispatcher: maps `state.step` to the right screen. |

---

## Screen / step state machine

`store.state.step` selects the screen. Transitions are driven by user actions
(bound in `app.bindEvents`) and by the `engine`.

```mermaid
stateDiagram-v2
  [*] --> style
  style --> upload: pick Selfie / Full-body
  upload --> camera: take photo
  camera --> preview: capture
  upload --> preview: file selected
  preview --> generating: Generate
  generating --> result: generation success
  generating --> preview: error
  result --> mix-match: Complete the look
  mix-match --> result: Try on
  result --> upload: Change the look
  upload --> style: back
```

---

## Integration contract (what the host page must provide)

The widget reads these globals from the page (mirrored by
`tryon-products-snippet.liquid` in production and by `local_test.html` locally):

- `window.DRESSX_TRYON`
  - `tryonEnabled` (bool, default true) - gate.
  - `imageUrl` / `frontImageUrl` - product image sent to the API (falls back to `#img-0 img`).
  - `features` - `{ fullBody, selfie, mixMatch }` (default all `true`). Loader may set via
    `data-dressx-features="fullbody" | "fullbody,selfie" | "fullbody,selfie,mixmatch"`.
    When `selfie` is false the widget skips the style screen and opens at upload (full-body).
    When `mixMatch` is false the "Complete the look" chip is hidden.
  - `currentProductId` - preselects the matching mix-and-match item.
  - `tryonProducts[]` - `{ id, category: 'top'|'bottom'|'shoes', title, image, image_ref?, price }`.
- `window.__DRESSX_TRYON_PARAMS` - product metadata: `product_title`, `price`,
  `variant_id`, `sku`, `product_category`, `product_brand`, `product_handle`,
  `product_collections`, `id`.
- A DOM element `#img-0` containing an `<img>` (button placement anchor).

---

## How to extend

**Add a new small component (atom):** add a pure function inside `ctx.ui`,
export it in the returned object, then use it from a view as `ui.myAtom({...})`.

**Add a new screen (view):** add a function inside `ctx.views`, wire it into
`renderScreen()` for a new `state.step` value, and add any needed
`data-action` handlers in `app.bindEvents`.

**Add a new API call:** add a `fetch` wrapper in `ctx.api` (keep it side-effect
free), then call it from `ctx.engine` or `ctx.actions`.

**Golden rule:** keep the dependency direction downward (UI reads state + calls
logic; logic calls api; nothing calls UI upward), and never mutate the DOM from
a view - views only return HTML strings; the app shell mounts them.

---

## Local testing

From this repo root:

```bash
python3 serve.py          # serves http://127.0.0.1:8123 with charset=utf-8
```

Open:
- combined build: http://127.0.0.1:8123/local_test.html
- original build: http://127.0.0.1:8123/local_test.html?v=original

Use the toggle in the top bar to compare the two builds screen-by-screen.

**Live Shopify PDP:** tunnel this folder (`serve.py` + ngrok). The theme loads
Django static `loader_shopifyapp_ngrok.js` from **`dx-tryon-shopify-app`**; set that
file’s `COMBINED_JS_URL` to your current tunnel URL for `try-on_shopifyapp_combined_test.js`.

> Note: both builds call the production API `api.dressxagent.com`. The final
> generation request may be rejected with **403** from `localhost` because the
> origin is not allow-listed. The full UI flow (screens, buttons, upload,
> pickers, mix-and-match) works locally; end-to-end generation must be tested
> from an allow-listed domain.

---

## Parity with the original

Behavioural equivalence was verified by loading both files in a mini-DOM and
walking the full user flow: **all screen states render byte-for-byte identical**
markup (style, upload x2, preview x2, picker, camera, result, mix-match), and
API endpoints, `data-action` hooks, analytics event names and the CSS all match.
