---
name: "Sales app extensibility"
repo: https://github.com/vtex/skills
source: https://github.com/vtex/skills/blob/HEAD/skills/sales-app-extensibility/SKILL.md
license: not specified
install: "npx skills add vtex/skills --skill sales-app-extensibility"
page: https://100xlab.com/resources/agent-skills/vtex-skills--sales-app-extensibility--su6rbv
---

# Sales App Extensibility

## When this skill applies

Use this skill when building, customizing, or deploying extensions for VTEX Sales App.

- Adding features to the cart page (promotions, loyalty, services)
- Adding features to the Product Detail Page (badges, recommendations, warranties)
- Adding features to the menu (user profile, navigation, metrics)
- Integrating external APIs into Sales App extensions
- Generating, scaffolding, or validating extension code for Sales App

Do not use this skill for:
- Regular FastStore storefront customization (use `faststore-storefront`)
- Building VTEX IO apps (use `vtex-io-*` skills)
- Sales App core development or framework modifications

### Prerequisite: FastStore project

The project root must contain: `biome.json`, `faststore.json`, `package.json`, `tsconfig.json`, `turbo.json`.

If any are missing, **STOP**. The user must install FastStore first:

```bash
npx @vtex/fsp-cli init
# Prompt: "What is the application name?" → enter name or press Enter for default
cd <application-name> && yarn
```

Documentation: https://beta.fast.store/getting-started

### Prerequisite: Sales App module

Inside the FastStore project, a Sales App workspace must exist (typically at `packages/sales-app`) with `src/`, `package.json`, and `tsconfig.json`. Check root `package.json` `"workspaces"` for a path containing `sales-app`. If missing, **STOP**:

```bash
yarn add @vtex/sales-app -D -W
npx fsp create
# Prompts: account name → "Sales App" → path (default or custom)
# Then add the path to root package.json "workspaces" array
yarn install
```

Documentation: https://beta.fast.store/sales-app/setting-up

**Do NOT proceed to discovery or code generation until both prerequisites are confirmed.**

## Decision rules

Follow the mandatory 7-step workflow (Steps 0–6) in order. Do not skip steps.

| Step | Purpose | Gate |
|------|---------|------|
| 0 | Check prerequisites | FastStore + Sales App installed → proceed; otherwise STOP |
| 1 | Discovery | Understand what the user wants to build |
| 2 | Requirements & Plan | Map requirements → generate plan → **wait for user approval** |
| 3 | Code Generation & Validation | Generate `Component.tsx` + `Component.css` (plain CSS, never `.module.css`) + `index.tsx` → validate |
| 4 | Documentation | Generate `docs/<ExtensionName>.md` explaining the extension |
| 5 | Local Testing | Provide dev commands and URLs |
| 6 | Build & Deploy | Build command → deployment guide |

### Extension point selection

| Extension Point | Category | Available Hooks | Layout Shift |
|----------------|----------|----------------|--------------|
| `cart.cart-list.after` | Cart | useCart, useExtension | No |
| `cart.cart-item.after` | Cart | useCart, useCartItem, useExtension | Yes |
| `cart.order-summary.after` | Cart | useCart, useExtension | Yes |
| `pdp.sidebar.before` | PDP | usePDP, useCart, useExtension | Yes |
| `pdp.sidebar.after` | PDP | usePDP, useCart, useExtension | Yes |
| `pdp.content.after` | PDP | usePDP, useCart, useExtension | Yes |
| `menu.item` | Menu | useExtension | No |
| `menu.drawer-content` | Menu | useCurrentUser, useExtension | No |

### Hook availability

- **useCart** → all cart + PDP extensions
- **useCartItem** → `cart.cart-item.after` only
- **useCurrentUser** → `menu.drawer-content` only
- **usePDP** → PDP extensions only
- **useExtension** → all extension points

### Template selection

- No API + no hooks → **simple template**
- No API + hooks → **hook template**
- API + no auth → **API template**
- API + VTEX IO proxy → **IO proxy template** (recommended)
- API + direct auth → **direct auth template** (insecure, warn user)
- API doc provided → generate TypeScript interfaces from extracted response shapes; no API doc → use `${DATA_INTERFACE}` placeholder

### API authentication strategy

1. **Recommended: VTEX IO Proxy App** — IO app stores keys server-side. Extension uses `credentials: 'include'` with a **relative path** (`/_v/my-api/data`).
2. **Insecure: Direct Auth** — Keys in frontend code, visible in browser. Testing/development only.

## Hard constraints

### Constraint: Component must return JSX.Element, never null

`defineExtensions` expects `ExtensionPointComponent` which returns `Element`, not `Element | null`.

**Why this matters**
Returning `null` causes a TypeScript compilation error. The build will fail.

**Detection**
`return null` in any component registered with `defineExtensions`.

**Correct**

```typescript
export function MyExtension(): JSX.Element {
  if (!data) return (<></>);
  return <div>{data.value}</div>;
}
```

**Wrong**

```typescript
export function MyExtension(): JSX.Element | null {
  if (!data) return null;
  return <div>{data.value}</div>;
}
```

### Constraint: Guard optional properties before use

`CartItem.manualPrice` (number | undefined), `CartItem.productRefId` (string | undefined), `CartItem.attachments` (Attachment[] | undefined) must be guarded.

**Why this matters**
TypeScript strict mode rejects accessing possibly-undefined values.

**Detection**
`item.manualPrice`, `item.productRefId`, or `item.attachments` without `?.`, `??`, `&&`, or `!= null`.

**Correct**

```typescript
const price = item.manualPrice ?? item.sellingPrice;
const refId = item.productRefId ?? 'N/A';
const count = item.attachments?.length ?? 0;
```

**Wrong**

```typescript
const price = item.manualPrice;
const refId = item.productRefId.toUpperCase();
const count = item.attachments.length;
```

### Constraint: useCartItem().item may be undefined

`item` from `useCartItem()` is `CartItem | undefined`.

**Why this matters**
Accessing properties on `undefined` causes a runtime crash.

**Detection**
Destructured `item` from `useCartItem()` used without `if (!item)` guard.

**Correct**

```typescript
const { item } = useCartItem();
if (!item) return (<></>);
return <div>{item.name}</div>;
```

**Wrong**

```typescript
const { item } = useCartItem();
return <div>{item.name}</div>;
```

### Constraint: defineExtensions is required in index.tsx

Entry point must use `defineExtensions` from `@vtex/sales-app`.

**Why this matters**
Without it, the build succeeds but no extensions render.

**Detection**
Missing `defineExtensions` import or call in `index.tsx`.

**Correct**

```typescript
import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';
export default defineExtensions({ 'cart.cart-list.after': MyExtension });
```

**Wrong**

```typescript
import { MyExtension } from './components/MyExtension';
export default { 'cart.cart-list.after': MyExtension };
```

### Constraint: Extension point names must match exactly

IDs are a fixed set. Non-existent names silently fail.

**Why this matters**
The extension renders nowhere. No build error — invisible at runtime.

**Detection**
Any name not in: `cart.cart-list.after`, `cart.cart-item.after`, `cart.order-summary.after`, `pdp.sidebar.before`, `pdp.sidebar.after`, `pdp.content.after`, `menu.item`, `menu.drawer-content`.

**Correct**

```typescript
defineExtensions({ 'cart.cart-list.after': MyExtension });
```

**Wrong**

```typescript
defineExtensions({ 'cart.list.after': MyExtension });
```

### Constraint: Hooks must be used in compatible extension points

Each hook has an `available_in` restriction.

**Why this matters**
Hook context is only mounted for specific extension points. Using elsewhere throws runtime errors.

**Detection**
`useCartItem` in PDP/menu. `useCurrentUser` in cart. `usePDP` in menu/cart-list.

**Correct**

```typescript
// useCartItem in cart.cart-item.after ✓
defineExtensions({ 'cart.cart-item.after': ItemWarranty });
```

**Wrong**

```typescript
// useCartItem in pdp.sidebar.after ✗ — will fail at runtime
defineExtensions({ 'pdp.sidebar.after': ItemWarranty });
```

## Preferred pattern

### Workflow overview

**Step 0** — Check FastStore + Sales App prerequisites. STOP if missing.

**Step 1** — Discovery. Detect use case from keywords, ask follow-up questions, determine API auth strategy. If the user provides API documentation (URL, OpenAPI/Swagger file, Markdown, or inline text), ingest it to extract endpoint details and response shapes — skip the equivalent manual questions. Validate extracted information with the user. Load the [discovery reference](references/discovery-and-use-cases.md) for detailed question flows and the API Documentation Ingestion section.

**Step 2** — Map requirements to extension point + hooks + template. Present plan listing the files to be created: `components/<ComponentName>.tsx`, `components/<ComponentName>.css` (plain CSS — **never** `.module.css`), and `index.tsx`. Wait for approval.

**Step 3** — Generate `<ComponentName>.tsx`, `<ComponentName>.css` (plain CSS, **never** `.module.css`), and `index.tsx`.

Required references for this step (load before generating):
- [code-templates-and-patterns.md](references/code-templates-and-patterns.md) — all component templates, the CSS template (with the full Sales App design system inlined: tokens, typography, spacing, responsive), the `index.tsx` template, type generation rules, and the custom fetch hook pattern.
- [extension-points-hooks-and-types.md](references/extension-points-hooks-and-types.md) — hook return types and TypeScript definitions.
- [static-analysis-rules.md](references/static-analysis-rules.md) — sandbox security, CSS containment, and React performance rules from `@vtex/fsp-analyzer`. Run these checks on all generated files.
- [design-guidelines.md](references/design-guidelines.md) — load when the extension renders any UI text (sentence case rule) or uses icons (Phosphor Icons).

If API documentation was ingested in Step 1, generate TypeScript interfaces from the extracted response shapes and use them in the component instead of the `${DATA_INTERFACE}` placeholder. If the extension calls 2+ endpoints, extract fetch logic into custom hook(s). Validate against the 12-point checklist in code-templates-and-patterns.md. Fix all violations before presenting code; surface warnings to the user.

**Step 4** — Generate `docs/<ExtensionName>.md` inside the Sales App package (create the `docs/` folder if needed). Load the [documentation template reference](references/documentation-template.md) for the required 9-section structure and the markdown skeleton to fill in.

**Step 5** — Provide local dev commands and test URLs. Load the [dev/build/deploy reference](references/local-dev-build-and-deploy.md).

**Step 6** — Build command and deployment guide. Load the [dev/build/deploy reference](references/local-dev-build-and-deploy.md).

## Reference Files

Load these on demand based on what the task requires. Do not load all of them upfront.

| File | Load when… |
|------|-----------|
| [references/extension-points-hooks-and-types.md](references/extension-points-hooks-and-types.md) | Choosing an extension point, selecting hooks, looking up TypeScript types (`CartItem`, `ProductSku`, `Totalizers`, `Attachment`), or checking hook return values and availability per extension point |
| [references/code-templates-and-patterns.md](references/code-templates-and-patterns.md) | Generating extension code — simple, hook, API, IO Proxy, or Direct Auth templates; **CSS template with the full Sales App design system inlined (tokens, typography, spacing, responsive)**; `index.tsx` with `defineExtensions`; hook initialization; validation checklist |
| [references/discovery-and-use-cases.md](references/discovery-and-use-cases.md) | Running Step 1 (Discovery) — use case detection keywords, follow-up questions, API auth decision tree, IO Proxy vs Direct Auth flow |
| [references/local-dev-build-and-deploy.md](references/local-dev-build-and-deploy.md) | Running Steps 5–6 — dev server commands, test URLs, build command, common build errors, FastStore WebOps deployment, monitoring, rollback |
| [references/static-analysis-rules.md](references/static-analysis-rules.md) | Validating generated code (Step 3) — sandbox security, CSS containment, and React performance rules from `@vtex/fsp-analyzer`; full rule catalog with violation IDs, detection patterns, and correct/wrong examples |
| [references/design-guidelines.md](references/design-guidelines.md) | Writing UI text (sentence case rule) or using icons (Phosphor Icons). **CSS-related design rules — tokens, typography, spacing, responsive — are inlined directly in the CSS template inside code-templates-and-patterns.md, so this file is not needed for CSS generation.** |
| [references/documentation-template.md](references/documentation-template.md) | Writing the `docs/<ExtensionName>.md` file in Step 4 — the 9-section structure and markdown skeleton |

## Common failure modes

- **Skipping prerequisite checks** — Generating code without FastStore/Sales App installed. Always confirm both before Step 1.
- **Not presenting plan** — User may want a different approach. Always confirm at Step 2 before generating code.
- **Skipping documentation** — Extension generated without `docs/<ExtensionName>.md`. Load documentation-template.md for the required structure.
- **Inventing API response types** — Generated interface doesn't match actual API. If documentation was provided, derive types from it; if not, ask the user for a sample JSON response.
- **Ignoring provided API documentation** — User provided a URL or file but agent asked manual questions anyway. Always check for documentation first and use the API Documentation Ingestion flow.
- **Inline fetch with 2+ endpoints** — Multiple `fetch` calls inside the component body. Extract into custom hook(s) in `hooks/use{Purpose}.ts`.
- **Placing code outside packages/sales-app/src/** — Files outside this path are not included in the build.

> Code-level violations (DOM APIs, Node imports, eval, CSS containment, React performance, design tokens) are enforced by `@vtex/fsp-analyzer` and caught during Step 3 validation. See [static-analysis-rules.md](references/static-analysis-rules.md) and [design-guidelines.md](references/design-guidelines.md).

## Review checklist

**Workflow gates**
- [ ] FastStore installed (biome.json, faststore.json, package.json, tsconfig.json, turbo.json)?
- [ ] Sales App module installed (src/, package.json, tsconfig.json in sales-app directory)?
- [ ] Discovery completed and use case identified?
- [ ] Execution plan approved by user?
- [ ] Extension point is valid (from the 8-point reference)?
- [ ] Hooks compatible with chosen extension point?
- [ ] Documentation generated at `docs/<ExtensionName>.md` (9 sections, using documentation-template.md)?
- [ ] If API documentation was provided, TypeScript interfaces match the documented response shape?
- [ ] If 2+ API endpoints used, fetch logic extracted into custom hook(s) in `hooks/`?
- [ ] Build passes: `yarn fsp build {account} sales-app`?
- [ ] Tested locally: `yarn fsp dev {account}`?

**TypeScript / runtime guards** (not caught by fsp-analyzer)
- [ ] Component returns JSX.Element, never null?
- [ ] Optional properties guarded (manualPrice, productRefId, attachments)?
- [ ] useCartItem().item checked for undefined?
- [ ] defineExtensions configured in index.tsx?

> All other code-level rules (sandbox APIs, CSS containment, React performance, design tokens) are enforced by `@vtex/fsp-analyzer` and checked during Step 3. See [static-analysis-rules.md](references/static-analysis-rules.md) and [design-guidelines.md](references/design-guidelines.md).

## Related skills

- `faststore-storefront` — storefront customization outside Sales App
- `vtex-io-app-contract` — building VTEX IO proxy apps for secure API integration

## Reference

- Sales App setup: https://beta.fast.store/sales-app/setting-up
- FastStore getting started: https://beta.fast.store/getting-started
- VTEX Sales App documentation: https://help.vtex.com/en/tracks/instore-getting-started-and-setting-up
- VTEX IO app development: https://developers.vtex.com/docs/guides/vtex-io-documentation-developing-an-app

---

## Supporting file: references/code-templates-and-patterns.md

# Code Templates and Patterns

## Template Selection

| Condition | Template |
|-----------|----------|
| No API + no hooks | Simple template |
| No API + hooks needed | Hook template |
| API + no authentication | API template |
| API + VTEX IO proxy app | IO Proxy template (recommended) |
| API + direct auth | Direct Auth template (insecure — warn user) |

## Simple Extension (no hooks, no API)

```typescript
import React from 'react';
import './${COMPONENT_NAME}.css';

/**
 * ${COMPONENT_NAME}
 * ${DESCRIPTION}
 *
 * Extension Point: ${EXTENSION_POINT}
 *
 * IMPORTANT: This component MUST always return a JSX.Element, never null.
 * The defineExtensions type (ExtensionPointComponent) does not accept null.
 * If you need to conditionally hide content, return an empty fragment: <></>
 */
export function ${COMPONENT_NAME}(): JSX.Element {
  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}
```

## Extension with Hooks

```typescript
import React from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

/**
 * ${COMPONENT_NAME}
 * ${DESCRIPTION}
 *
 * Extension Point: ${EXTENSION_POINT}
 * Hooks: ${HOOKS_LIST}
 */
export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}
```

### Hook initialization patterns

```typescript
// useCart — access cart data and mutations
const cart = useCart();
// cart.items, cart.value, cart.totalizers, cart.clientProfileData, cart.giftCards
// cart.addItem(), cart.removeItem(), cart.addCoupon(), cart.addGiftCard(), cart.sync()

// useCartItem — access individual cart item (cart.cart-item.after ONLY)
const { item, itemIndex, changeItem, changePrice } = useCartItem();
// ALWAYS check: if (!item) return (<></>);

// useCurrentUser — access logged-in user (menu.drawer-content ONLY)
const { name, email } = useCurrentUser();

// useExtension — access account and extension point context (ALL extension points)
const { account, extensionPoint } = useExtension();

// usePDP — access product data (PDP extensions ONLY)
const { productSku } = usePDP();
// productSku.id, productSku.name, productSku.price, productSku.listPrice
```

## Extension with API (no auth)

```typescript
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null)
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: 'GET',
          headers: { 'Content-Type': 'application/json' },
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}
```

## Extension with IO Proxy (recommended for authenticated APIs)

The IO app handles the external API keys securely on the server side. The extension uses `credentials: 'include'` to forward session cookies.

**Critical**: The API endpoint must be a **relative path** (e.g., `/_v/my-api/data`). **NEVER** use a full URL like `https://{account}.myvtex.com/...`. The Sales App internal proxy resolves the domain automatically.

```typescript
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: '${API_METHOD}',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          ${FETCH_BODY}
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}
```

## Extension with Direct Auth (insecure — testing only)

**⚠️ SECURITY WARNING**: This template passes authentication keys directly in request headers. The keys are visible to anyone inspecting the browser's network requests. Use a VTEX IO proxy app in production.

```typescript
import React, { useState, useEffect } from 'react';
import { ${HOOKS_IMPORT} } from '@vtex/sales-app';
import './${COMPONENT_NAME}.css';

/**
 * ⚠️ SECURITY WARNING: Authentication keys exposed in frontend code.
 * Move to a VTEX IO proxy app for production use.
 */

interface ${COMPONENT_NAME}Data {
  // Generated from API documentation — see docs/<ExtensionName>.md for source
  // Object fields → typed properties. Nested objects → separate interfaces. Optional fields → property?: Type
  ${DATA_INTERFACE}
}

export function ${COMPONENT_NAME}(): JSX.Element {
  ${HOOKS_USAGE}
  const [data, setData] = useState<${COMPONENT_NAME}Data | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('${API_ENDPOINT}', {
          method: '${API_METHOD}',
          headers: {
            'Content-Type': 'application/json',
            // ⚠️ INSECURE: Authentication key exposed in frontend code
            '${AUTH_HEADER_NAME}': '${AUTH_HEADER_VALUE}',
          },
          ${FETCH_BODY}
        });

        if (!response.ok) throw new Error('Failed to fetch data');
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  if (loading) {
    return (
      <div className="container">
        <div className="loading">
          <span className="spinner"></span>
          <span>Loading...</span>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container">
        <div className="error"><span>Error: {error}</span></div>
      </div>
    );
  }

  if (!data) return (<></>);

  return (
    <div className="container">
      <div className="content">
        ${CONTENT}
      </div>
    </div>
  );
}
```

## CSS Stylesheet

All extensions use **plain CSS** (not CSS modules). The full Sales App design system for CSS — tokens, typography, spacing grid, responsive breakpoints, and structure — is inlined below. **Use this template as the single source of truth for any CSS generation.**

**CRITICAL — File naming:** The CSS file MUST be named `${COMPONENT_NAME}.css` — **never** `${COMPONENT_NAME}.module.css`. The Sales App bundler does not support CSS modules. Import as a side-effect: `import './${COMPONENT_NAME}.css';` — never as `import styles from './${COMPONENT_NAME}.module.css';`. Use `className="container"` string literals, not `className={styles.container}`.

### Design system rules baked into this template

These rules apply to **every** generated extension CSS. The template below already complies — keep it that way.

| Topic | Rule |
|-------|------|
| Tokens | All colors must reference `--sa-color-*` tokens declared on `.container`. **Never** use hardcoded hex values anywhere else. |
| Font family | `'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif` on `.container`, inherited by descendants. |
| Font weights | Only Regular (400), Medium (500), Semibold (600), Bold (700). |
| Font sizes (px) | Allowed scale only: **10, 12, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 44, 48**. Never `13`, `15`, `17`, etc. |
| Typography roles | Section title = 16px/600 · Subtitle/label = 14px/500 · Body = 14px/400 · Caption = 12px/400 · Price = 18px/600. |
| Spacing | Multiples of **4px** only (4, 8, 12, 16, 20, 24, 32, 40…). Never `10`, `14`, `18`, `22`. Applies to `padding`, `margin`, `gap`, `height`. |
| Containment | No `position: fixed`, no `z-index: 9999`, no `:host`, no `:global`, no `@import`. `position: relative` and `sticky` are safe. |
| Selectors | Use the unprefixed utility class names from this template (`.container`, `.content`, `.row`, `.title`, `.button`, etc.). The Sales App `fsp-analyzer` runs with `transformNonCompliant: true`, so unprefixed selectors emit a `CSS_TRANSFORMED` warning rather than a `CSS_NAMESPACE_REQUIRED` violation. Never use `*`, `body`, `html`, `:root`, `head`, `main`, `#root`, `#__next` — those are caught by `CSS_GLOBAL_SELECTOR` and remain blocking. |
| Keyframes | Prefix with `${COMPONENT_NAME}-` (e.g., `${COMPONENT_NAME}-spin`). This raises a `CSS_TRANSFORMED` warning (build still passes) — use `sales-app-extension-${COMPONENT_NAME}-spin` if you want a clean run. |
| `!important` | Never. |
| Responsive | Use the breakpoint at `(max-width: 743px)` for mobile overrides. Sales App shell handles the rest. |

```css
/**
 * ${COMPONENT_NAME} Styles
 * Sales App design system (tokens + typography + spacing + responsive) is fully inlined.
 * UX writing: Use sentence case for all UI text. Never use ALL-CAPS.
 */

.container {
  /* === Sales App design tokens — single source of truth ===
     Always declare these on .container. Reference via var(--sa-color-*) in all other rules.
     Never use hardcoded hex values in CSS rules. */

  /* Primary action */
  --sa-color-primary:       #157BF4;   /* Light Blue 800 */
  --sa-color-primary-hover: #0366DD;   /* Light Blue 900 */

  /* Text */
  --sa-color-text-primary:   #1F1F1F;  /* Neutral 1200 — high emphasis */
  --sa-color-text-secondary: #5C5C5C;  /* Neutral 600  — medium emphasis */
  --sa-color-text-tertiary:  #3D3D3D;  /* Neutral 500  — low emphasis */
  --sa-color-text-muted:     #999999;  /* Neutral 700  — disabled / hint */

  /* Backgrounds */
  --sa-color-bg:        #FFFFFF;       /* Neutral White */
  --sa-color-bg-subtle: #F5F5F5;       /* Neutral 100 */
  --sa-color-bg-muted:  #EBEBEB;       /* Neutral 200 */

  /* Borders */
  --sa-color-border:       #E0E0E0;    /* Neutral 300 */
  --sa-color-border-input: #D6D6D6;    /* Neutral 400 */

  /* Error (Red scale) */
  --sa-color-error-bg:     #FDF6F5;    /* Red 50 */
  --sa-color-error-border: #FFDFD9;    /* Red 200 */
  --sa-color-error-text:   #EC3727;    /* Red 800 */

  /* Success (Green scale) */
  --sa-color-success-bg:   #EDFDF5;    /* Green 50 */
  --sa-color-success-text: #01905F;    /* Green 800 */

  /* Warning (Yellow / Orange scale) */
  --sa-color-warning-bg:   #FBF7D4;    /* Yellow 50 */
  --sa-color-warning-text: #E57001;    /* Orange 700 */

  /* Info (Light Blue scale) */
  --sa-color-info-bg:   #F1F8FD;       /* Light Blue 50 */
  --sa-color-info-text: #157BF4;       /* Light Blue 800 */

  padding: 16px;
  background-color: var(--sa-color-bg);
  font-family: 'VTEX Trust', -apple-system, BlinkMacSystemFont, sans-serif;
}

/* Layout */
.content { display: flex; flex-direction: column; gap: 12px; }
.row { display: flex; align-items: center; gap: 12px; }
.spaceBetween { justify-content: space-between; }

/* Typography — uses approved size scale (10, 12, 14, 16, 18, 20, 22, 24...) */
.title    { font-size: 16px; font-weight: 600; color: var(--sa-color-text-primary);   margin: 0; }
.subtitle { font-size: 14px; font-weight: 500; color: var(--sa-color-text-secondary); margin: 0; }
.text     { font-size: 14px; color: var(--sa-color-text-tertiary); line-height: 1.5; }

/* Loading state */
.loading {
  display: flex; align-items: center; justify-content: center;
  gap: 8px; padding: 24px; color: var(--sa-color-text-secondary);
}
.spinner {
  width: 20px; height: 20px;
  border: 2px solid var(--sa-color-border); border-top-color: var(--sa-color-primary);
  border-radius: 50%; animation: ${COMPONENT_NAME}-spin 1s linear infinite;
}
@keyframes ${COMPONENT_NAME}-spin { to { transform: rotate(360deg); } }

/* Error state */
.error {
  padding: 16px; background-color: var(--sa-color-error-bg);
  border: 1px solid var(--sa-color-error-border); border-radius: 8px;
  color: var(--sa-color-error-text); font-size: 14px;
}

/* Button styles */
.button {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 12px 16px; font-size: 14px; font-weight: 500;
  border: none; border-radius: 6px; cursor: pointer;
  transition: background-color 0.2s ease;
}
.buttonPrimary { background-color: var(--sa-color-primary); color: #FFFFFF; }
.buttonPrimary:hover { background-color: var(--sa-color-primary-hover); }
.buttonSecondary { background-color: var(--sa-color-bg-muted); color: var(--sa-color-text-tertiary); }
.buttonSecondary:hover { background-color: var(--sa-color-border); }

/* Card */
.card {
  padding: 16px; background-color: var(--sa-color-bg-subtle);
  border-radius: 8px; border: 1px solid var(--sa-color-border);
}

/* Badges */
.badge {
  display: inline-block; padding: 4px 8px;
  font-size: 12px; font-weight: 500; border-radius: 4px;
}
.badgeSuccess { background-color: var(--sa-color-success-bg); color: var(--sa-color-success-text); }
.badgeWarning { background-color: var(--sa-color-warning-bg); color: var(--sa-color-warning-text); }
.badgeInfo    { background-color: var(--sa-color-info-bg);    color: var(--sa-color-info-text); }

/* Input */
.input {
  width: 100%; padding: 12px; font-size: 14px;
  border: 1px solid var(--sa-color-border-input); border-radius: 6px;
  outline: none; transition: border-color 0.2s ease;
}
.input:focus { border-color: var(--sa-color-primary); }

/* Price display */
.price          { font-size: 18px; font-weight: 600; color: var(--sa-color-text-primary); }
.priceOld       { font-size: 14px; color: var(--sa-color-text-muted); text-decoration: line-through; }
.priceDiscount  { font-size: 14px; color: var(--sa-color-success-text); font-weight: 500; }

/* Responsive — Sales App breakpoints
   Small (mobile)  320–743px  → margin: 24px, no column grid
   Medium (tablet) 744–1279px → host gutters: 16px (no override needed)
   Large (desktop) 1280–1919px → host gutters: 20px (no override needed)
   Extra large     1920px+    → host gutters: 20px (no override needed) */
@media (max-width: 743px) {
  .container { padding: 12px; margin: 0 24px; }
}
```

### CSS file structure

Every extension CSS must follow this order:

1. **Token declarations** — `--sa-color-*` on `.container`
2. **Layout helpers** — `.container`, `.content`, `.row`, `.spaceBetween`
3. **Typography** — `.title`, `.subtitle`, `.text`
4. **State** — `.loading`, `.spinner`, `@keyframes`, `.error`
5. **Component classes** — `.button`, `.card`, `.badge`, `.input`, `.price`
6. **Responsive** — `@media` queries at the bottom

For UX writing rules (sentence case) and iconography (Phosphor Icons) used in `.tsx`, see [design-guidelines.md](design-guidelines.md).

## index.tsx with defineExtensions

```typescript
import { defineExtensions } from '@vtex/sales-app';
${COMPONENT_IMPORTS}

/**
 * Sales App Extensions Entry Point
 *
 * Connects extension components to their target extension points.
 * Each extension point can only have one component assigned to it.
 *
 * Available extension points:
 * - cart.cart-list.after
 * - cart.cart-item.after
 * - cart.order-summary.after
 * - pdp.sidebar.before
 * - pdp.sidebar.after
 * - pdp.content.after
 * - menu.item
 * - menu.drawer-content
 */
export default defineExtensions({
  ${EXTENSION_MAPPINGS}
});
```

## Validation Rules

After generating code, validate for these issues:

1. **React import present** — `import React from 'react'` is required for JSX compilation
2. **Component exported** — must have `export function`, `export const`, or `export default`
3. **No null returns** — `return null` is not allowed; use `return (<></>)` instead
4. **Hook imports** — all hooks must be imported from `@vtex/sales-app`
5. **Optional property guards** — `manualPrice`, `productRefId`, `attachments` must have null checks
6. **useCartItem item check** — `if (!item) return (<></>)` before accessing item properties
7. **Hook/extension-point compatibility** — each hook is only valid in specific extension points
8. **API call handling** — loading state and error handling must be present for any `fetch()` call
9. **CSS class usage** — CSS classes defined in the stylesheet should be used in the component
10. **defineExtensions in index.tsx** — entry point must import and call `defineExtensions`
11. **Static analysis compliance** — generated code must pass all fsp-analyzer sandbox security, CSS containment, and React performance rules. Load the [static analysis reference](static-analysis-rules.md) to run the full check. Fix all violations before presenting code to the user; flag warnings to the user for review.
12. **Design system compliance** — the CSS file must follow the rules table at the top of the "CSS Stylesheet" section above: `--sa-color-*` custom properties declared on `.container` and used in all rules, `'VTEX Trust'` font family, allowed font sizes (10, 12, 14, 16, 18, 20, 22, 24, 28, 32…), 4px-multiple spacing, scoped selectors, and the responsive override at `(max-width: 743px)`. For UI text (sentence case) and icons (Phosphor), see [design-guidelines.md](design-guidelines.md).

## API Type Generation from Documentation

When API documentation was ingested during Discovery (Step 1), generate TypeScript interfaces from the extracted response shapes. Apply these rules:

### JSON to TypeScript conversion rules

| JSON value | TypeScript type |
|-----------|----------------|
| `"string"` | `string` |
| `123` | `number` |
| `true` / `false` | `boolean` |
| `null` or sometimes absent | `type \| null` or `property?: type` |
| `{ }` (nested object) | Separate named `interface` |
| `[ ]` (array of objects) | `InterfaceType[]` |
| `[ ]` (array of primitives) | `string[]`, `number[]`, etc. |

### Naming conventions

- Top-level response type: `{ComponentName}Data` (replaces the `${DATA_INTERFACE}` placeholder)
- Nested object type: `{ComponentName}{FieldName}` (e.g., `LoyaltyHistoryEntry`)
- Request body type (POST/PUT): `{ComponentName}Request`
- Keep names PascalCase and domain-specific

### Example

Given API response:

```json
{ "points": 100, "tier": "gold", "expiresAt": null, "history": [{ "date": "2026-01-01", "amount": 10 }] }
```

Generate:

```typescript
interface LoyaltyHistoryEntry {
  date: string;
  amount: number;
}

interface LoyaltyData {
  points: number;
  tier: string;
  expiresAt: string | null;
  history: LoyaltyHistoryEntry[];
}
```

If a field is documented as optional (not always present), use `?`:

```typescript
interface ProductRecommendationData {
  id: string;
  name: string;
  price: number;
  imageUrl?: string;  // optional per API docs
  badge?: string;     // optional per API docs
}
```

### Multiple endpoints

If the extension calls more than one endpoint, generate a separate interface per endpoint response. Prefix with the endpoint purpose: `{ComponentName}{Purpose}Data` (e.g., `LoyaltyBalanceData`, `LoyaltyRedemptionResponse`).

## Custom Fetch Hook Pattern

Use this pattern when the extension calls **2 or more endpoints** or when the fetch logic is complex (chained requests, pagination, polling).

### Decision rule

- **1 endpoint** → inline `useEffect` fetch inside the component (existing pattern).
- **2+ endpoints** → extract into one or more custom hooks in `hooks/use{Purpose}.ts`.

### Custom hook template

```typescript
import { useState, useEffect } from 'react';

export function use${Purpose}(account: string) {
  const [data, setData] = useState<${ResponseType} | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        const response = await fetch('${API_ENDPOINT}', {
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
        });
        if (!response.ok) throw new Error('Failed to fetch ${Purpose}');
        const result: ${ResponseType} = await response.json();
        setData(result);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An error occurred');
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [account]);

  return { data, loading, error };
}
```

### Usage in component

```typescript
import { use${Purpose} } from './hooks/use${Purpose}';

export function ${COMPONENT_NAME}(): JSX.Element {
  const { account } = useExtension();
  const { data: balance, loading: loadingBalance, error: errorBalance } = use${Purpose}(account);
  const { data: history, loading: loadingHistory, error: errorHistory } = use${OtherPurpose}(account);

  if (loadingBalance || loadingHistory) return <div className="loading">Loading...</div>;
  if (errorBalance || errorHistory) return <div className="error">Error loading data</div>;
  if (!balance || !history) return (<></>);

  return (
    <div className="container">
      {/* render using balance and history */}
    </div>
  );
}
```

### File placement

Custom hooks go in `packages/sales-app/src/hooks/use{Purpose}.ts`. They must be TypeScript-only files (no JSX, no `.tsx` extension needed unless they return JSX).

---

## Supporting file: references/design-guidelines.md

# Sales App Design Guidelines (non-CSS)

This reference covers two topics that apply to **TSX content**, not CSS:

1. **UX writing rules** — for any text rendered in JSX
2. **Iconography** — when the extension uses icons

> **For CSS:** all design tokens, typography scale, spacing grid, and responsive breakpoints are inlined directly in the CSS template at [code-templates-and-patterns.md](code-templates-and-patterns.md) §"CSS Stylesheet". That template is the single source of truth for any CSS generation. Do not invent or duplicate token values here.

---

## 1. UX Writing Rules

Apply to every string that appears in the UI (labels, buttons, messages, placeholders, alt text).

| Rule | Correct | Wrong |
|------|---------|-------|
| Sentence case | "Add to cart" | "ADD TO CART" |
| Proper nouns capitalized | "VTEX account" | "vtex account" |
| No all-caps | "Loyalty points" | "LOYALTY POINTS" |
| No system-style placeholders | "Your email" | "EMAIL ADDRESS" |
| No CSS uppercase transforms | (omit `text-transform: uppercase`) | `text-transform: uppercase` |

These rules derive from Google Material Design and Apple Human Interface Guidelines, adopted by VTEX.

---

## 2. Iconography

### Library

All icons must come from **[Phosphor Icons](https://phosphoricons.com/)**. Do not use Material Icons, Font Awesome, Heroicons, or any other library.

### Installation

```bash
yarn add @phosphor-icons/react
```

### Sizes

| Usage | Size |
|-------|------|
| Compact / inline | 16px |
| Standard / default | 24px |
| Large / featured | 32–48px |

Always use sizes within **16px to 48px**.

### Weights

Use **Regular** or **Bold** weight variants. Keep the weight consistent within a single component.

### Usage example

```typescript
import { ShoppingCart, Star, Warning } from '@phosphor-icons/react';

export function MyExtension(): JSX.Element {
  return (
    <div className="container">
      <ShoppingCart size={24} />
      <span>Cart items</span>
    </div>
  );
}
```

---

## 3. Quick checklist

Before presenting generated `.tsx` to the user, verify:

- [ ] All UI text in sentence case (no ALL-CAPS, no system-style placeholders)?
- [ ] All icons imported from `@phosphor-icons/react`?
- [ ] Icon sizes between 16px and 48px (24px default)?
- [ ] Icon weights consistent within the component?

For CSS verification (tokens, fonts, spacing, responsive), see [code-templates-and-patterns.md](code-templates-and-patterns.md) §"Design system rules baked into this template".

---

## Supporting file: references/discovery-and-use-cases.md

# Discovery Flow and Use Cases

## Use Case Detection

Detect the use case from keywords in the user's description:

| Use Case | Keywords | Complexity | Requires API | Requires Hooks |
|----------|----------|------------|-------------|----------------|
| Static Information | message, warning, banner, badge, text, static, fixed | Simple | No | No |
| Loyalty Program | loyalty, points, benefit, redeem, cashback, program | Medium | Yes | Yes |
| Additional Services | warranty, insurance, installation, assembly, service, extra | Medium | Yes | Yes |
| Product Recommendations | recommendation, suggestion, related, complementary, cross-sell, upsell | High | Yes | Yes |
| Custom Discounts | discount, coupon, promotion, price, voucher | High | Yes | Yes |
| User Profile Display | profile, user, seller, menu, drawer | Simple | No | Yes |

## Discovery Questions by Use Case

### Static Information

1. **Content type**: Text message, Banner/Card, Badge/Tag, List of information, or Alert/Warning?
2. **Location**: Which extension point?
   - `cart.cart-list.after` — After the cart items list
   - `cart.order-summary.after` — After the order summary
   - `pdp.sidebar.before` — Before the PDP sidebar
   - `pdp.sidebar.after` — After the PDP sidebar
   - `pdp.content.after` — After the PDP content
3. **Content**: What text/information to display?

**Recommended extension points**: `cart.cart-list.after`, `cart.order-summary.after`, `pdp.sidebar.before`
**Required hooks**: none
**Template**: Simple

### Loyalty Program

0. **API documentation** (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 2–5 — the agent will extract endpoint, parameters, and response types automatically.
1. **Data source**: Own external API, VTEX IO app, VTEX Master Data, Third-party API, or Not sure yet?
2. **API endpoint**: Path (e.g., `/_v/my-loyalty/points`) — only if external data source and no docs provided
3. **HTTP method**: GET, POST, PUT, or DELETE? — skip if API docs provided
4. **Request params**: Parameters, query strings, or request body to send — skip if API docs provided
5. **Response format**: JSON structure of the API response — skip if API docs provided
6. **Features**: Display points balance, Show earnings with purchase, Allow points redemption, Display customer level
7. **Location**: `cart.order-summary.after` or `menu.drawer-content`
8. **Interaction**: View only or Interactive (redeem points, apply discounts)?

**Recommended extension points**: `cart.order-summary.after`, `menu.drawer-content`
**Required hooks**: useCart, useExtension, useCurrentUser
**Template**: API (IO Proxy or Direct Auth)

### Additional Services

0. **API documentation** (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
1. **Service types**: Extended warranty, Insurance, Installation, Assembly, Customization, Other
2. **Data source**: External API, VTEX IO app, Fixed list in code, or VTEX Catalog?
3. **API endpoint**: Path — only if external data source and no docs provided
4. **HTTP method**: GET, POST, PUT, or DELETE? — skip if API docs provided
5. **Request params**: Parameters or request body — skip if API docs provided
6. **Response format**: JSON structure of the API response — skip if API docs provided
7. **Location**: `cart.cart-item.after` (per item) or `pdp.sidebar.after` (product page)
8. **Pricing model**: Fixed price, Percentage of product, or Calculated by API?

**Recommended extension points**: `cart.cart-item.after`, `pdp.sidebar.after`
**Required hooks**: useCart, useCartItem, usePDP
**Template**: Hook or API (depending on data source)

### Product Recommendations

0. **API documentation** (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
1. **Recommendation type**: Frequently bought together, Accessories, Similar products, Upgrades
2. **Data source**: Own API, VTEX Intelligent Search, Fixed rules, or Third-party API?
3. **API endpoint**: Path — only if external data source and no docs provided
4. **HTTP method**: GET, POST, PUT, or DELETE? — skip if API docs provided
5. **Request params**: Parameters or request body — skip if API docs provided
6. **Response format**: JSON structure of the API response — skip if API docs provided
7. **Location**: `cart.cart-list.after` or `pdp.content.after`
8. **Quick add**: Can the seller quickly add recommended products to the cart?
9. **Display count**: 3, 4, 6 products, or customizable?

**Recommended extension points**: `cart.cart-list.after`, `pdp.content.after`
**Required hooks**: useCart, usePDP, useExtension
**Template**: API (IO Proxy or Direct Auth)

### Custom Discounts

0. **API documentation** (optional, recommended): Do you have API documentation to share? URL, OpenAPI/Swagger JSON/YAML, Markdown file path, or paste the spec inline. If yes, skip questions 3–6 — the agent will extract endpoint, parameters, and response types automatically.
1. **Discount types**: Discount coupon, Manual seller discount, Volume discount, Special promotion
2. **Validation source**: Own promotions API, VTEX Promotions, or Fixed coupon list?
3. **API endpoint**: Path for validation — only if external API and no docs provided
4. **HTTP method**: GET, POST, PUT, or DELETE? — skip if API docs provided
5. **Request params**: Parameters or request body — skip if API docs provided
6. **Response format**: JSON structure of the API response — skip if API docs provided
7. **Discount limits**: No limit, Maximum percentage, Maximum value, or Depends on seller level?
8. **Approval needed**: No, Yes above a certain value, or Always?

**Recommended extension points**: `cart.order-summary.after`
**Required hooks**: useCart, useCurrentUser, useExtension
**Template**: API (IO Proxy or Direct Auth)

### User Profile Display

1. **Information to display**: Name, Email, Sales metrics, Goals, Quick settings
2. **Metrics source** (if metrics selected): No metrics, Internal API, VTEX OMS, or Static data?

**Recommended extension points**: `menu.drawer-content`
**Required hooks**: useCurrentUser, useExtension
**Template**: Hook (no API if only using useCurrentUser data)

## API Authentication Decision Tree

When the use case requires an external API:

### Step 1: Check for API documentation and confirm API details

**First** — Ask: does the user have API documentation they can share?

- **Yes** → Go to the [API Documentation Ingestion](#api-documentation-ingestion) section. Extract HTTP method, request parameters/body, and response JSON structure from the docs. Present the extracted details to the user for confirmation, then proceed to Step 2.
- **No** → Collect manually: HTTP method, request parameters/body, and API response JSON structure. Proceed to Step 2.

### Step 2: Does the API need authentication?

- **No** → Use the basic API template. Done.
- **Yes** → Continue to Step 3.

### Step 3: Does the user have a VTEX IO proxy app?

A VTEX IO proxy app acts as a middleware: the Sales App extension calls the IO app using `credentials: 'include'` to forward session cookies, and the IO app calls the external API with the secret keys on the server side. The keys never leave the server.

- **Yes** → Collect the IO app endpoint path (relative, starting with `/`). Use IO Proxy template. Done.
- **No** → Continue to Step 4.

### Step 4: Implement IO proxy first, or continue with insecure direct auth?

- **Implement IO proxy first** → Stop the Sales App extension workflow. The user should build the IO proxy app first and return later.
- **Continue with insecure direct auth** → Continue to Step 5.

### Step 5: Security warning and direct auth details

**⚠️ WARNING**: Passing authentication keys directly in frontend code is **NOT secure**. The keys will be visible to anyone inspecting the browser. Confirm the user understands and accepts the risk.

If confirmed, collect:
- Auth header name (e.g., `x-api-key`, `Authorization`)
- Auth header value (e.g., `Bearer your-token`, `your-api-key`)
- Full API endpoint URL

Use the Direct Auth template.

### IO Proxy Critical Rule

The `fetch` call in the extension must use **ONLY the relative path** (e.g., `/_v/my-api/endpoint`). **NEVER** prefix with `https://` or a domain like `{account}.myvtex.com`. The Sales App has an internal proxy that automatically resolves the domain.

## API Documentation Ingestion

When the user provides API documentation, follow these steps to extract structured information before generating code.

### Supported input formats

| Format | How to load | Notes |
|--------|------------|-------|
| URL | Use `fetch_webpage` to retrieve the page content | Works for REST API reference pages, Swagger UI, Redoc |
| OpenAPI/Swagger JSON | Parse the JSON directly from user input or file content | Look for `paths`, `components/schemas` keys |
| OpenAPI/Swagger YAML | Convert YAML mentally to JSON structure, then parse same as above | |
| Markdown doc | Read sections for endpoint paths, method, headers, request/response examples | |
| Inline text | Read the pasted content, identify patterns: `GET /path`, `POST /path`, JSON examples | |

### Extraction checklist

From any documentation format, extract and record:

1. **Base URL** — the host/prefix (e.g., `https://api.example.com/v2`). For IO Proxy, this becomes irrelevant — only the path matters.
2. **Endpoints** — for each endpoint: HTTP method, path, and purpose description.
3. **Required headers** — `Content-Type`, `Authorization`, `x-api-key`, or others.
4. **Query parameters** — name, type, required/optional.
5. **Request body shape** — JSON structure with field names, types, and required/optional status.
6. **Response shape** — JSON structure. If multiple status codes, capture the success response (200/201).
7. **Error shapes** — if documented, capture 4xx/5xx response structures.

### Extraction output (mental model)

After extracting, summarize to the user in this format before proceeding to code generation:

```
Extracted API summary:
- Endpoint: [METHOD] [path]
- Auth: [None / IO Proxy at /_v/... / Direct with header X]
- Request: { field: type, ... }
- Response: { field: type, ... }
- Optional fields: [list]

Does this match what you expected?
```

Wait for user confirmation before proceeding to Step 2 (template selection) or code generation.

### Multiple endpoints

If the documentation describes multiple endpoints needed by the extension:
- List all of them in the summary above
- Note whether they can be called in parallel or must be sequential
- This will determine whether a custom fetch hook is needed (see code templates reference)

---

## Supporting file: references/documentation-template.md

# Extension Documentation Template

Generate `docs/<ExtensionName>.md` inside the Sales App package. Create the `docs/` folder if it does not exist.

The document must contain these 9 sections:

1. **Extension name** — title matching the component name.
2. **Overview** — one-paragraph summary of what the extension does and why it was built.
3. **Extension point** — which extension point it registers on (e.g., `cart.cart-list.after`) and why that point was chosen.
4. **Hooks used** — list each hook (`useCart`, `usePDP`, etc.) with a brief explanation of what data it provides to this extension.
5. **Component structure** — description of the component tree, props, and state management.
6. **Styling** — CSS file name and summary of key classes.
7. **API integration** (if applicable) — endpoint, auth strategy (IO Proxy or direct), request/response shape.
   - **Source documentation:** URL or file path of the original API documentation, if provided.
   - **Generated types:** list of TypeScript interfaces generated from the API documentation.
   - Note which fields are optional (`?`) vs required per the documentation.
8. **How to test** — dev server command and URL to reach the extension.
9. **Known constraints** — any guards, edge cases, or limitations (e.g., `useCartItem().item` may be undefined).

```markdown
# <ExtensionName>

## Overview
<One-paragraph description of the extension purpose and value.>

## Extension point
- **Point:** `<extension.point.name>`
- **Rationale:** <Why this extension point was selected.>

## Hooks used
| Hook | Purpose |
|------|---------|
| `useCart` | <What data it provides here> |

## Component structure
<Describe the component tree, key props, and internal state.>

## Styling
- **File:** `<ComponentName>.css`
- <Summary of key CSS classes and design decisions.>

## API integration
- **Endpoint:** `/_v/...`
- **Auth strategy:** IO Proxy / Direct / None
- **Request/Response:** <Brief shape description.>

## How to test
Run `yarn fsp dev {account}` and navigate to `https://{account}.myvtex.com/sales-app/...` to verify the extension renders.

## Known constraints
- <Guard or limitation 1>
- <Guard or limitation 2>
```

---

## Supporting file: references/extension-points-hooks-and-types.md

# Extension Points, Hooks, and Types

## Extension Points

Sales App has 8 extension points across 3 categories.

### Cart Extension Points

#### `cart.cart-list.after`

- **Location**: Below the cart items list
- **Layout Shift**: No
- **Available Hooks**: useCart, useExtension
- **Use Cases**: Loyalty points summary, promotional banners, gift options, shipping estimates

```typescript
import { defineExtensions } from '@vtex/sales-app';
import { MyExtension } from './components/MyExtension';

export default defineExtensions({
  'cart.cart-list.after': MyExtension,
});
```

#### `cart.cart-item.after`

- **Location**: Below each individual cart item
- **Layout Shift**: Yes — use loading states and skeletons
- **Available Hooks**: useCart, useCartItem, useExtension
- **Use Cases**: Warranty options, item-specific promotions, personalization, service attachments

```typescript
export default defineExtensions({
  'cart.cart-item.after': ItemWarranty,
});
```

#### `cart.order-summary.after`

- **Location**: Below the order summary/totals
- **Layout Shift**: Yes — use loading states and skeletons
- **Available Hooks**: useCart, useExtension
- **Use Cases**: Loyalty points redemption, additional fees/discounts, coupon input, installment options

```typescript
export default defineExtensions({
  'cart.order-summary.after': LoyaltyPoints,
});
```

### PDP Extension Points

#### `pdp.sidebar.before`

- **Location**: Above the sidebar content on the Product Detail Page
- **Layout Shift**: Yes
- **Available Hooks**: usePDP, useCart, useExtension
- **Use Cases**: Product badges, stock alerts, promotional banners, seller information

#### `pdp.sidebar.after`

- **Location**: Below the sidebar content on the Product Detail Page
- **Layout Shift**: Yes
- **Available Hooks**: usePDP, useCart, useExtension
- **Use Cases**: Warranty selection, related services, financing options, gift wrapping

#### `pdp.content.after`

- **Location**: Below the main content area on the Product Detail Page
- **Layout Shift**: Yes
- **Available Hooks**: usePDP, useCart, useExtension
- **Use Cases**: Product recommendations, customer reviews, specifications, complementary products

### Menu Extension Points

#### `menu.item`

- **Location**: Sales App main menu
- **Layout Shift**: No
- **Available Hooks**: useExtension
- **Use Cases**: Custom navigation links, quick action buttons, menu notifications

#### `menu.drawer-content`

- **Location**: Sales App menu drawer
- **Layout Shift**: No
- **Available Hooks**: useCurrentUser, useExtension
- **Use Cases**: User profile information, sales performance metrics, settings shortcuts, notifications

## Hooks Reference

All hooks are imported from `@vtex/sales-app`.

### useCart

Access cart data and perform mutations.

```typescript
import { useCart } from '@vtex/sales-app';

const cart = useCart();
```

**Available in**: `cart.cart-list.after`, `cart.cart-item.after`, `cart.order-summary.after`, `pdp.sidebar.before`, `pdp.sidebar.after`, `pdp.content.after`

**Returns**:

| Property | Type | Description |
|----------|------|-------------|
| `orderFormId` | `string \| undefined` | Unique identifier of the current order form |
| `value` | `number` | Total cart value in cents |
| `items` | `CartItem[]` | Array of items in the cart |
| `totalizers` | `Totalizers[]` | Array of totalizers (Items, Shipping, Discounts, Tax) |
| `clientProfileData` | `ClientProfileData` | Client profile (email, document, phone) |
| `giftCards` | `GiftCard[]` | Gift cards currently attached to the cart |
| `addItem` | `(data: UseCartAddItemData) => Promise<void>` | Add an item to the cart |
| `removeItem` | `(id: string, index: number) => Promise<void>` | Remove an item from the cart |
| `addCoupon` | `(coupon: string) => Promise<void>` | Add a coupon to the cart |
| `addGiftCard` | `(redemptionCodeOrGiftCard: string \| GiftCard, provider?: string) => Promise<void>` | Add a gift card to the cart payment data |
| `sync` | `() => Promise<void>` | Sync the cart with the latest Order Form data |

**Example — display total items**:

```typescript
const TotalItems = () => {
  const cart = useCart();
  return <div>Items: {cart.items.length}</div>;
};
```

**Example — add item to cart**:

```typescript
const AddToCart = () => {
  const cart = useCart();
  const addItem = () => cart.addItem({
    quantity: 1,
    seller: '1',
    id: '8392'
  });
  return <button onClick={addItem}>Add to cart</button>;
};
```

**Example — add gift card**:

```typescript
const AddGiftCard = () => {
  const cart = useCart();
  const add = async () => {
    await cart.addGiftCard('ABC-123', 'my-giftcard-provider');
    await cart.sync();
  };
  return <button onClick={add}>Add gift card</button>;
};
```

### useCartItem

Access individual cart item data. **Only available in `cart.cart-item.after`**.

```typescript
import { useCartItem } from '@vtex/sales-app';

const { item, itemIndex, changeItem, changePrice } = useCartItem();
// IMPORTANT: item may be undefined — always check first
if (!item) return (<></>);
```

**Returns**:

| Property | Type | Description |
|----------|------|-------------|
| `item` | `CartItem \| undefined` | The cart item data — **check for undefined** |
| `itemIndex` | `number \| undefined` | Index of the item in the cart |
| `changeItem` | `(data: UseCartItemChangeItemData) => Promise<void>` | Modify quantity or attachments |
| `changePrice` | `(price: number) => Promise<void>` | Change item price (requires `allowManualPrice` in orderForm) |

### useCurrentUser

Access current authenticated user data. **Only available in `menu.drawer-content`**.

```typescript
import { useCurrentUser } from '@vtex/sales-app';

const { name, email } = useCurrentUser();
```

**Returns**:

| Property | Type | Description |
|----------|------|-------------|
| `name` | `string` | Current user's name |
| `email` | `string` | Current user's email |

### useExtension

Access account and extension point context. **Available in all extension points**.

```typescript
import { useExtension } from '@vtex/sales-app';

const { account, extensionPoint } = useExtension();
```

**Returns**:

| Property | Type | Description |
|----------|------|-------------|
| `account` | `string` | Current VTEX account name |
| `extensionPoint` | `ExtensionPointsType` | Name of the extension point where the component is mounted |

### usePDP

Access Product Detail Page data. **Only available in PDP extensions**.

```typescript
import { usePDP } from '@vtex/sales-app';

const { productSku } = usePDP();
```

**Returns**:

| Property | Type | Description |
|----------|------|-------------|
| `productSku` | `ProductSku` | Current product SKU data |

## TypeScript Types

```typescript
type CartItem = {
  id: string;
  name: string;
  quantity: number;
  seller: string;
  sellingPrice: number;
  listPrice: number;
  manualPrice?: number;       // optional — guard before use
  price: number;
  imageUrl: string;
  productRefId?: string;      // optional — guard before use
  attachments?: Attachment[];  // optional — guard before use
};

type ClientProfileData = {
  email: string | null;
  document: string | null;
  phone: string | null;
};

type ProductSku = {
  id: string;
  name: string;
  quantity: number;
  price: number;
  listPrice: number;
  sellingPrice?: number;
};

type Totalizers = {
  id: string;    // Common IDs: "Items", "Shipping", "Discounts", "Tax"
  name: string;
  value: number; // in cents
};

type Attachment = {
  name: string;
  content: Record<string, string>;
};

type GiftCard = {
  id: string;
  redemptionCode?: string | null;
  name: string;
  caption: string;
  value: number;
  balance: number;
  provider: string;
  groupName?: string | null;
  inUse: boolean;
  isSpecialCard: boolean;
};

type UseCartAddItemData = {
  id: string;
  quantity: number;
  seller: string;
  attachments?: Attachment[];
};

type UseCartItemChangeItemData = {
  quantity?: number;
  attachments?: Attachment[];
};

type ExtensionPointsType =
  | 'cart.cart-list.after'
  | 'cart.cart-item.after'
  | 'cart.order-summary.after'
  | 'pdp.sidebar.before'
  | 'pdp.sidebar.after'
  | 'pdp.content.after'
  | 'menu.item'
  | 'menu.drawer-content';
```

### Optional Properties Warning

Three `CartItem` properties are optional and must be guarded:

| Property | Type | Safe Access |
|----------|------|-------------|
| `manualPrice` | `number \| undefined` | `item.manualPrice ?? item.sellingPrice` |
| `productRefId` | `string \| undefined` | `item.productRefId ?? 'N/A'` |
| `attachments` | `Attachment[] \| undefined` | `item.attachments?.length ?? 0` |

---

## Supporting file: references/local-dev-build-and-deploy.md

# Local Development, Build, and Deployment

## Local Development (Step 4)

### Start the dev server

```bash
# Verify dependencies are installed
yarn install

# Primary command — run at monorepo root
yarn fsp dev {account_name}

# Alternative with more control
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002

# Show extension point placeholders (useful for finding where to render)
yarn sales-app dev {account_name} ./{account_name}/sales-app 3002 --show-placeholders
```

### Test URLs

| Page | URL |
|------|-----|
| Cart | `http://localhost:3002/sales-app/checkout/cart` |
| PDP | `http://localhost:3002/sales-app/product/<product-id>` |
| Base | `http://localhost:3002/sales-app` |

### Development tips

- Hot reload is active — code changes reflect automatically
- Use `--show-placeholders` to see where extension points are located
- Check the port configuration in `faststore.json` if there are conflicts

### Troubleshooting

| Issue | Solution |
|-------|----------|
| Port in use | Try a different port: 3003, 3004 |
| Extension not visible | Check `index.tsx` and the configured extension point name |
| Build error during dev | Run `yarn fsp build {account_name} sales-app` for detailed errors |
| Module not found | Run `yarn install` to ensure dependencies are linked |

## Build (Step 5a)

### Build command

```bash
yarn fsp build {account_name} sales-app
```

### What the build checks

1. **TypeScript** — types and syntax errors
2. **Imports** — referenced modules and files exist
3. **JSX** — correct React component syntax
4. **CSS** — style files imported correctly
5. **defineExtensions** — valid extension points configuration

### Common build errors

| Error | Cause | Solution |
|-------|-------|----------|
| TypeScript errors | Type mismatches, missing types | Check variable types and `@vtex/sales-app` imports |
| Module not found | Wrong import path or missing file | Verify file exists at the imported path |
| Hook errors | Hooks inside conditionals or loops | Move hook calls to the top level of the component |
| CSS errors | Missing or misnamed CSS file | Check `.css` file exists and import path is correct |
| defineExtensions errors | Invalid extension point or missing export | Verify extension point name and component export |

### Build tip

During development, `yarn fsp dev` already runs the build automatically. Use `yarn fsp build` when you want to validate before deploying.

## Deployment (Step 5b)

Deployment is automatic via **FastStore WebOps** when you push to the `main` branch.

### Deployment steps

```bash
# 1. Verify local build passes
yarn fsp build {account_name} sales-app

# 2. Review changes
git status

# 3. Stage files
git add .

# 4. Create commit
git commit -m "feat: add Sales App extension"

# 5. Push to main
git push origin main
```

After pushing:

6. **Monitor deploy** — Access FastStore WebOps in VTEX Admin (`https://{account_name}.myvtex.com/admin/faststore-webops`)
7. **Wait for propagation** — Deployment takes 5-10 minutes (includes CDN cache propagation)
8. **Validate in production** — Access Sales App in production and verify the extension

### Monitoring

**FastStore WebOps**:
- URL: VTEX Admin > FastStore > WebOps
- Shows build and deployment status

**GitHub Checks**:
- Go to the GitHub repository
- See the status icon (✔️, ❌, 🟡) next to each commit

### Rollback

If something goes wrong after deployment:

```bash
git revert HEAD
git push origin main
```

### Troubleshooting deployment

#### Build failed in WebOps

1. Go to the GitHub repository
2. Click the status icon next to the commit
3. Select "Details" under "FastStore WebOps"
4. Read the error logs
5. Fix the issue locally
6. Make a new commit and push

#### Extension doesn't appear after deploy

1. Check the extension point is correct in `index.tsx`
2. Clear browser cache (Ctrl+Shift+R)
3. Wait longer for propagation (up to 10 minutes)
4. Verify you're accessing the correct account

#### Runtime error in production

1. Open the browser console (F12)
2. Check for JavaScript errors
3. If API error, verify URLs and authentication
4. Fix locally, test, and redeploy

---

## Supporting file: references/static-analysis-rules.md

# Static Analysis Rules (fsp-analyzer)

Sales App extensions run inside a sandboxed environment enforced by `@vtex/fsp-analyzer`. These rules are checked at `preBuild` and `preDev` hook time. The AI must validate all generated code against these rules before presenting it to the user.

Rule IDs match the `ViolationKind`, `ReactViolationKind`, `WarningKind`, and `ReactWarningKind` types from `@vtex/fsp-analyzer` so developers can cross-reference with real build output.

---

## How to apply these rules

After generating a component, CSS file, and `index.tsx`, check each generated file against the **violation** rules (block generation — fix before presenting) and the **warning** rules (flag to the user with a suggestion). Do not present code that has unresolved violations.

---

## 1. Sandbox Security Rules

Applied to `.ts` and `.tsx` files by `FastStoreSandboxAnalyzer`.

### `RESTRICTED_API` — DOM API access banned

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted identifiers: `document`, `window`, `localStorage`, `sessionStorage`, `navigator`.

**Detection:** Any use of these identifiers as a value (not as a type import).

**Correct**
```typescript
// Use React state and props for data; use useExtension() for account context
const { account } = useExtension();
```

**Wrong**
```typescript
const userId = localStorage.getItem('userId');
const width = window.innerWidth;
document.title = 'My Extension';
```

---

### `VARIABLE_ALIASING` — Aliasing restricted APIs

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Assigning a restricted API to a variable to circumvent detection.

**Detection:** `const x = document`, `const [w, d] = [window, document]`, or any destructuring that aliases a restricted API.

**Correct**
```typescript
// Pass data via props or React context, not via aliased globals
```

**Wrong**
```typescript
const doc = document;
const [w, d] = [window, document];
doc.querySelector('.item');
```

---

### `GLOBAL_MANIPULATION` — Global object manipulation

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted member expressions: `window.location`, `window.history`.

**Detection:** Member expressions on `window` to access `location` or `history`.

**Correct**
```typescript
// Navigation is managed by Sales App shell; do not manipulate location
```

**Wrong**
```typescript
window.location.href = '/new-path';
window.location.reload();
```

---

### `HISTORY_MANIPULATION` — Browser history manipulation

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted calls: `window.history.pushState`, `window.history.replaceState`, `history.pushState`, `history.replaceState`.

**Detection:** Any call to these methods.

**Correct**
```typescript
// Let the Sales App shell handle routing
```

**Wrong**
```typescript
history.pushState({}, '', '/cart');
window.history.replaceState(null, '', '/pdp');
```

---

### `DYNAMIC_SCRIPT_CREATION` — Dynamic script creation

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted expressions: `document.createElement`, `document.write`, `document.writeln`, `document.body.appendChild`, `document.head.appendChild`.

**Detection:** Any call to these methods.

**Correct**
```typescript
// Render UI via JSX; never inject scripts dynamically
```

**Wrong**
```typescript
const script = document.createElement('script');
script.src = 'https://cdn.example.com/lib.js';
document.body.appendChild(script);
```

---

### `CODE_EXECUTION` — Arbitrary code execution

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted calls: `eval`, `Function` (as constructor), `setTimeout` (string form), `setInterval` (string form), `new Function`.

**Detection:** Any call or instantiation of these identifiers.

> **Note on `setTimeout`/`setInterval`:** These are restricted in the sandbox to prevent arbitrary code injection. Use `useEffect` with `AbortController` or cleanup functions for timing-based logic:

**Correct**
```typescript
useEffect(() => {
  const id = window.setTimeout(() => { /* logic */ }, 500);
  return () => window.clearTimeout(id);
}, []);
// Note: even this should be reviewed; prefer event-driven patterns
```

**Wrong**
```typescript
eval('doSomething()');
const fn = new Function('return 42');
setTimeout('doSomething()', 1000);
```

---

### `RESTRICTED_IMPORT` — Restricted Node.js module imports

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted modules: `fs`, `path`, `child_process`, `crypto`.

**Detection:** Any `import` statement importing from these module names.

**Correct**
```typescript
// Extensions are browser-only React components; do not import Node.js modules
import { useCart } from '@vtex/sales-app';
```

**Wrong**
```typescript
import fs from 'fs';
import { resolve } from 'path';
import crypto from 'crypto';
```

---

### `RESTRICTED_IMPORT_SOURCE` — Direct node_modules path imports

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

**Detection:** Import paths containing `node_modules/`.

**Wrong**
```typescript
import something from 'node_modules/lodash/get';
```

---

### `NON_APPROVED_PACKAGE` — Import from non-approved package

**Severity:** Warning (flag)  
**Files:** `.ts`, `.tsx`

Any import whose source starts with `@` or contains `/`, unless it is `react` or a `node_modules/` direct path. Fires for every `@vtex/*` scoped package and every relative import (`./foo`, `../bar`).

> **Note:** This warning is noisy — it fires for all legitimate Sales App imports (`@vtex/sales-app`, `@phosphor-icons/react`, relative component paths). Do not surface to the user when the import is from `@vtex/*` or a relative path. Investigate only when the import is from an unexpected third-party package.

**Detection:** `import { … } from 'source'` where `source.startsWith('@')` or `source.includes('/')`, and source is not `'react'` or a `node_modules/` path.

---

### `EXTERNAL_RESOURCE_LOADING` — Unrestricted external resource loading

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Two detection paths:

1. **Direct API use:** `fetch`, `XMLHttpRequest`, `script.src`, `link.href` used as member expressions.
2. **Known CDN domains in string literal:** `fetch('https://cdn.jsdelivr.net/...')` or any URL containing `cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`, `ajax.googleapis.com`, `code.jquery.com`, `api.example.com`, or ending with `.js`.

> **Exception:** `fetch` via the IO Proxy relative path (`/_v/...`) is allowed and required for secure API integration.

**Correct**
```typescript
const response = await fetch('/_v/my-loyalty-api/points', {
  credentials: 'include',
});
```

**Wrong**
```typescript
const response = await fetch('https://api.example.com/data');
const response = await fetch('https://cdn.jsdelivr.net/npm/lodash.js'); // CDN — also Violation
const xhr = new XMLHttpRequest();
```

---

### `EXTERNAL_RESOURCE_FETCH` — Fetch to known CDN domains

> **Note:** This rule ID is declared in the analyzer type system but is never raised by any handler. CDN domain fetches are actually reported as `EXTERNAL_RESOURCE_LOADING` (Violation, block) — not as this Warning. Treat any fetch to a CDN domain as a **build-blocking Violation**, not a soft warning.

**Severity:** Warning (flag) — *see note above; actual enforcement is via `EXTERNAL_RESOURCE_LOADING`*  
**Files:** `.ts`, `.tsx`

Known third-party domains: `cdn.jsdelivr.net`, `unpkg.com`, `cdnjs.cloudflare.com`, `ajax.googleapis.com`, `code.jquery.com`.

**Detection:** `fetch()` first argument literal containing one of these domains.

---

### `QUERY_SELECTOR_USAGE` — DOM query selectors

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted calls: `document.getElementById`, `document.getElementsByClassName`, `document.getElementsByTagName`, `document.querySelector`, `document.querySelectorAll`, and element-scoped variants.

**Detection:** Any call where the callee matches `document.querySelector`, `document.getElementById`, etc. (the variable must be literally named `document`). Calls via a ref object — e.g. `containerRef.current.querySelector(...)` — are **not** caught by the analyzer and must be avoided as a Hard Constraint.

**Correct**
```typescript
// Use React refs for direct DOM access when absolutely needed
const ref = useRef<HTMLDivElement>(null);
```

**Wrong**
```typescript
const el = document.querySelector('.my-class');
document.getElementById('cart-container');
```

---

### `STYLE_MANIPULATION` — Direct style manipulation

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

Restricted expressions: `element.style`, `element.className`, `element.classList`, `element.setAttribute.style`, `element.setAttribute.class`, `element.hidden`, `element.display`, `element.visibility`.

**Detection:** Matches only when the object is literally named `element` (e.g. `element.style`). Equivalent access through other variable names — e.g. `divRef.current.style`, `el.className` — is **not** caught by the analyzer and must be avoided as a Hard Constraint.

**Correct**
```typescript
// Use plain CSS classes from the component stylesheet
import './MyExtension.css';

return <div className={isActive ? 'active' : 'inactive'} />;
```

**Wrong**
```typescript
element.style.color = 'red';
element.classList.add('active');
element.setAttribute('style', 'display: none');
```

---

### `INFINITE_LOOP` — Infinite loops

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

**Detection:** `while(true)` or `do...while(true)` with literal `true` condition.

**Wrong**
```typescript
while (true) {
  processItem();
}
```

---

### `POTENTIAL_INFINITE_LOOP` — Incomplete for-loop

**Severity:** Warning (flag)  
**Files:** `.ts`, `.tsx`

**Detection:** `for` statement with no `update` expression AND no `test` expression (e.g. `for (let i = 0;;) { … }`).

**Action:** Warn the user and ensure the loop has a proper termination condition.

---

### `MEMORY_LEAK` — Memory leak patterns

> **Note:** This rule ID is declared in the analyzer type system but is never raised by any handler. The active rule is `POTENTIAL_MEMORY_LEAK` (Warning) below. Event listeners without cleanup **will not block the build**, but remain a Hard Constraint — always add a cleanup return in `useEffect`.

**Severity:** Violation (block) — *see note above; not currently enforced at build time*  
**Files:** `.ts`, `.tsx`

**Detection:** Event listeners, subscriptions, or timers added without cleanup in `useEffect`.

**Correct**
```typescript
useEffect(() => {
  const handler = () => { /* ... */ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);
```

**Wrong**
```typescript
useEffect(() => {
  window.addEventListener('resize', handler);
  // Missing cleanup — memory leak
}, []);
```

---

### `POTENTIAL_MEMORY_LEAK` — Likely memory leak patterns

**Severity:** Warning (flag)  
**Files:** `.ts`, `.tsx`

Three triggers:
1. Any `addEventListener` call — the analyzer warns regardless of whether a corresponding `removeEventListener` exists in cleanup.
2. Recursive function call where the callee name matches the parent `FunctionDeclaration` identifier.
3. `Array.push` inside a `for` or `while` loop.

**Action:** Verify the component has a proper cleanup return in `useEffect` for any event listener, and that recursive functions have a clear base case.

**Correct**
```typescript
useEffect(() => {
  const handler = () => { /* ... */ };
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);
```

---

### `EXCESSIVE_API_CALLS` — Excessive API call patterns

**Severity:** Violation (block)  
**Files:** `.ts`, `.tsx`

**Detection:** `fetch` or `XMLHttpRequest` calls whose parent is a `for` or `while` loop.

**Wrong**
```typescript
// fetch inside loop — excessive calls
for (const item of items) {
  await fetch(`/_v/api/item/${item.id}`);
}
```

---

### `FREQUENT_API_CALLS` — API call outside loop / rapid setInterval

**Severity:** Warning (flag)  
**Files:** `.ts`, `.tsx`

Two triggers:
1. `fetch` or `XMLHttpRequest` call **outside** a loop (i.e. anywhere else in the component). Fires for every fetch — ensure calls are wrapped in `useEffect` with proper deps and are rate-limited or cached.
2. `setInterval` with a numeric delay argument below 1000 ms.

> **Note:** Trigger 1 is intentionally broad — it flags every `fetch()` call as a reminder to verify it is not running unintentionally on every render. It does **not** mean every fetch is wrong. Only surface this to the user when the fetch is genuinely outside a `useEffect` or appears to run on each render.

**Action:** Warn the user and suggest wrapping fetches in `useEffect`, using caching, or increasing the polling interval.

---

### `LARGE_BUNDLE_SIZE_IMPACT` — Bundle size impacts

**Severity:** Warning (flag)  
**Files:** `.ts`, `.tsx`

**Detection:**
- String literals longer than 1,000 characters
- Object literals with more than 100 properties
- Imports from heavyweight libraries: `lodash`, `moment`

**Action:** Warn the user and suggest alternatives (`date-fns` instead of `moment`, native array methods instead of `lodash`).

**Correct**
```typescript
import { format } from 'date-fns';
const formattedDate = format(new Date(), 'yyyy-MM-dd');
```

**Wrong**
```typescript
import moment from 'moment';
import _ from 'lodash';
```

---

## 2. CSS Containment Rules

Applied to `.css`, `.scss`, `.less` files by `CSSAnalyzer`.

**Sales App analyzer configuration** (from `@vtex/sales-app` build/dev hook):

```typescript
cssOptions: {
  allowedNamespaces: ['sales-app-extension-'],
  defaultNamespace: 'sales-app-extension-',
  transformNonCompliant: true,
  overwriteTransformed: true,
}
```

Two consequences for the rules below:

1. The configured namespace is **`sales-app-extension-`**, not the analyzer's bare default of `extension-`. Any rule that talks about "the allowed namespace" means `sales-app-extension-` in this project.
2. `transformNonCompliant: true` softens the namespace rules: when a selector or `@keyframes` name is not prefixed, the analyzer auto-rewrites it into a sibling `*.transformed.css` file and emits a `CSS_TRANSFORMED` **warning** instead of a `CSS_NAMESPACE_REQUIRED` / `CSS_GLOBAL_KEYFRAMES` **violation**. The original `.css` (and the `className` strings in JSX) are left untouched, so the build does not break. This is why the CSS template in [code-templates-and-patterns.md](code-templates-and-patterns.md) ships unprefixed selectors like `.container`, `.title`, `.row` — they are valid in this project.

The other CSS rules (`CSS_GLOBAL_SELECTOR`, `CSS_CONTAINMENT_BREAKOUT`, `CSS_GLOBAL_IMPORT`, `CSS_RESTRICTED_PROPERTY`, `CSS_RESTRICTED_VALUE`, and the inline-style rules) are **not** softened by `transformNonCompliant` — they remain build-blocking violations.

### `CSS_GLOBAL_SELECTOR` — Global element selectors

**Severity:** Violation (block)  
**Files:** `.css`

Restricted selectors: `*`, `body`, `html`, `:root`, `head`, `main`, `#root`, `#__next`.

**Detection:** Any CSS rule whose selector matches or contains these elements at the top level.

**Correct**
```css
/* Scope rules inside the extension container — never target global elements */
.container {
  font-size: 14px;
}

.container .title {
  font-weight: bold;
}
```

**Wrong**
```css
body {
  font-size: 14px;
}

* {
  box-sizing: border-box;
}

:root {
  --my-color: red;
}
```

---

### `CSS_CONTAINMENT_BREAKOUT` — CSS containment breakout

**Severity:** Violation (block)  
**Files:** `.css`

Restricted patterns: `:host`, `:host-context`, `::slotted`, `:global`, `position: fixed`, `position: absolute`, `z-index: 9999`.

**Detection:** Any rule or declaration matching these patterns.

> **Note on positioning:** `position: relative` and `position: sticky` within the extension container are generally safe. Avoid `fixed` and unbounded `absolute` that escape the extension boundary.

**Correct**
```css
.container {
  position: relative; /* safe — contained */
}
```

**Wrong**
```css
.overlay {
  position: fixed;
  z-index: 9999;
  top: 0;
  left: 0;
}

:host {
  display: block;
}
```

---

### `CSS_NAMESPACE_REQUIRED` — Missing CSS namespace

**Severity:** Warning in this project (auto-transformed) — would be Violation if `transformNonCompliant` were `false`  
**Files:** `.css`

All CSS selectors should be scoped under an allowed namespace prefix. In the Sales App, that prefix is `sales-app-extension-`. Because the project sets `transformNonCompliant: true`, an unprefixed selector does **not** block the build: the analyzer rewrites it inside a sibling `Component.transformed.css` and emits a `CSS_TRANSFORMED` warning instead of `CSS_NAMESPACE_REQUIRED`. The original `Component.css` (which is what gets bundled) keeps the unprefixed class, so the JSX `className="container"` continues to match.

**Detection:** A selector that does not include any value from `allowedNamespaces` (substring check on `.namespace`, `#namespace`, or `[class*="namespace"]`).

**Correct (and matches the CSS template)**
```css
/* Unprefixed top-level classes are accepted because transformNonCompliant=true. */
.container { padding: 8px; }
.title     { font-size: 16px; }
```

**Also correct (explicit prefix, never warns)**
```css
.sales-app-extension-container { padding: 8px; }
.sales-app-extension-title     { font-size: 16px; }
```

> Prefer the unprefixed form to match the CSS template in [code-templates-and-patterns.md](code-templates-and-patterns.md) and the JSX in the component templates. The `CSS_TRANSFORMED` warning that fires for these is informational only.

---

### `CSS_GLOBAL_IMPORT` — CSS @import rules

**Severity:** Violation (block)  
**Files:** `.css`

`@import` pulls external stylesheets that affect global styles and bypass containment.

**Detection:** Any `@import` at-rule anywhere in the CSS file.

**Correct**
```css
/* Import a local CSS variable file via bundler config, not @import */
```

**Wrong**
```css
@import url('https://fonts.googleapis.com/css2?family=Roboto');
@import './reset.css';
```

---

### `CSS_GLOBAL_KEYFRAMES` — Keyframes without namespace

**Severity:** Warning in this project (auto-transformed) — would be Violation if `transformNonCompliant` were `false`  
**Files:** `.css`

`@keyframes` names are global. Without a namespace they may conflict with other extensions or the Sales App shell. As with `CSS_NAMESPACE_REQUIRED`, `transformNonCompliant: true` rewrites the name in `*.transformed.css` and emits `CSS_TRANSFORMED` instead of blocking.

**Detection:** `@keyframes` whose name does not start with an allowed namespace prefix (`sales-app-extension-` in this project).

The CSS template prefixes keyframes with `${COMPONENT_NAME}-` (e.g. `LoyaltyPoints-spin`). That prefix does **not** start with `sales-app-extension-`, so it triggers a `CSS_TRANSFORMED` warning — the build still succeeds, but the transformed file will rename it to `sales-app-extension-LoyaltyPoints-spin`. Either form is acceptable.

**Correct (no warning)**
```css
@keyframes sales-app-extension-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
```

**Also correct (CSS_TRANSFORMED warning, build passes)**
```css
@keyframes LoyaltyPoints-spin {
  to { transform: rotate(360deg); }
}
```

---

### `CSS_RESTRICTED_PROPERTY` — Restricted CSS properties

**Severity:** Violation (block)  
**Files:** `.css`

Certain properties affect layout or rendering globally and are restricted to prevent layout shift or containment breakout.

**Detection:** Declarations using restricted property names (e.g., properties that set global layout hints).

**Action:** If detected, remove or scope the declaration appropriately.

---

### `CSS_RESTRICTED_VALUE` — Restricted CSS values

**Severity:** Violation (block)  
**Files:** `.css`

Certain CSS values (e.g., `!important`) override the cascade and break containment guarantees.

**Detection:** `!important` in any declaration, or values targeting global layout tokens.

**Correct**
```css
.button {
  color: var(--sa-color-primary);
}
```

**Wrong**
```css
.button {
  color: red !important;
}
```

---

### `CSS_RESTRICTED_INLINE_PROPERTY` — Restricted property in inline style

**Severity:** Violation (block)  
**Files:** `.tsx`

Inline `style={{ … }}` props in JSX may not use properties that affect layout or containment.

Restricted properties: `position`, `z-index`, `top`, `left`, `right`, `bottom`, `all`, `contain`, `content`, `isolation`.

**Detection:** A JSX `style` prop object containing one of the restricted property names.

**Correct**
```typescript
// Use CSS classes for layout properties
return <div className="overlay" />;
```

**Wrong**
```typescript
return <div style={{ position: 'fixed', top: 0 }} />;
```

---

### `CSS_RESTRICTED_INLINE_VALUE` — Restricted value in inline style

**Severity:** Violation (block)  
**Files:** `.tsx`

Inline `style={{ … }}` props may not use values that break containment or cascade.

Restricted values: `fixed`, `absolute`, `!important`, `inherit`, `initial`, `unset`.

**Detection:** A JSX `style` prop object containing one of the restricted values.

**Wrong**
```typescript
return <div style={{ position: 'fixed' }} />;
return <div style={{ display: 'inherit' }} />;
```

---

## 3. React Performance Rules

Applied to `.tsx` files by `ReactPerformanceAnalyzer`. Violations block presentation; warnings are flagged to the user.

### `REACT_UNNECESSARY_RERENDER` — State update inside loop

**Severity:** Violation (block)  
**Files:** `.tsx`

State setter calls inside `for` or `while` loops trigger a re-render on every iteration.

**Detection:** A `setState` / `set*` call whose parent node is a `for` or `while` statement.

**Correct**
```typescript
// Compute derived value, then set once
const updatedItems = items.map(transform);
setItems(updatedItems);
```

**Wrong**
```typescript
for (const item of items) {
  setCount((c) => c + 1); // re-renders on every iteration
}
```

---

### `REACT_ANONYMOUS_COMPONENT` — Anonymous (lowercase) component

**Severity:** Violation (block)  
**Files:** `.tsx`

Components assigned to variables starting with lowercase are treated as HTML tags by React and cannot be used in JSX.

**Detection:** Arrow function or function expression assigned to a variable that does not start with an uppercase letter.

**Correct**
```typescript
export function LoyaltyPoints(): JSX.Element { ... }
export const WarrantyBadge = (): JSX.Element => { ... };
```

**Wrong**
```typescript
const loyaltyPoints = (): JSX.Element => { ... };
const warrantyBadge = function() { ... };
```

---

### `REACT_DIRECT_DOM_MANIPULATION` — Direct DOM manipulation in React

**Severity:** Violation (block)  
**Files:** `.tsx`

Bypasses the React virtual DOM and causes unpredictable behavior.

**Detection:** Calls to `document.querySelector` or `document.getElementById` inside a React component.

**Correct**
```typescript
const containerRef = useRef<HTMLDivElement>(null);
// Access DOM via containerRef.current
```

**Wrong**
```typescript
const el = document.querySelector('.cart-list');
el.style.display = 'none'; // bypasses React
```

---

### `REACT_LAYOUT_EFFECT_MISUSE` — useLayoutEffect usage

**Severity:** Violation (block)  
**Files:** `.tsx`

`useLayoutEffect` blocks the browser paint and can cause performance issues in a sandboxed extension.

**Detection:** Any call to `useLayoutEffect`.

**Correct**
```typescript
useEffect(() => {
  // DOM reading/writing after paint
}, []);
```

**Wrong**
```typescript
useLayoutEffect(() => {
  // blocks paint
}, []);
```

---

### `REACT_MISSING_DEPS` — Missing dependency array

**Severity:** Warning (flag)  
**Files:** `.tsx`

`useEffect`, `useCallback`, or `useMemo` without a second argument re-runs on every render.

**Detection:** A call to one of these hooks with fewer than 2 arguments, or with a non-array second argument.

**Correct**
```typescript
useEffect(() => {
  fetchData();
}, [productId]); // explicit deps
```

**Wrong**
```typescript
useEffect(() => {
  fetchData(); // runs on every render
});
```

---

### `REACT_INLINE_FUNCTION` — Inline functions in JSX props

**Severity:** Warning (flag)  
**Files:** `.tsx`

Arrow functions defined directly in JSX attributes create a new function reference on every render, preventing child memoization.

**Detection:** Function expression or arrow function expression whose parent is a JSX expression container or prop.

**Correct**
```typescript
const handleClick = useCallback(() => {
  doSomething();
}, []);

return <button onClick={handleClick}>Click</button>;
```

**Wrong**
```typescript
return <button onClick={() => doSomething()}>Click</button>;
```

---

### `REACT_LARGE_COMPONENT` — Component over 200 lines

**Severity:** Warning (flag)  
**Files:** `.tsx`

Large components are harder to maintain and test. Threshold: **200 lines**.

**Detection:** A function/arrow function with start/end line difference > 200.

**Action:** Suggest splitting into smaller sub-components or extracting logic into custom hooks.

---

### `REACT_MANY_USESTATE` — More than 5 useState calls

**Severity:** Warning (flag)  
**Files:** `.tsx`

Many `useState` calls in a single component indicate complex local state. Threshold: **5 calls**.

**Detection:** More than 5 calls to `useState` (or `React.useState`) within a single function component.

**Action:** Suggest consolidating related state into a single `useState` with an object, or extracting to a custom hook.

**Correct**
```typescript
const [formState, setFormState] = useState({
  name: '',
  email: '',
  quantity: 1,
});
```

**Wrong**
```typescript
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [submitted, setSubmitted] = useState(false); // 6th useState — flagged
```

---

### `REACT_CONDITIONAL_HOOK` — Hook called conditionally

**Severity:** Warning (flag)  
**Files:** `.tsx`

Calling a hook inside an `if`, `switch`, or ternary expression violates the Rules of Hooks.

**Detection:** A hook call (useEffect, useState, useMemo, useCallback, useLayoutEffect) whose ancestor is an `IfStatement`, `SwitchStatement`, or `ConditionalExpression`.

**Correct**
```typescript
const cart = useCart(); // always called unconditionally

if (!cart) return <></>;
```

**Wrong**
```typescript
if (isEnabled) {
  const cart = useCart(); // conditional hook call — violates Rules of Hooks
}
```

---

### `REACT_COMPLEX_JSX` — Deep JSX nesting

**Severity:** Warning (flag)  
**Files:** `.tsx`

Deep JSX trees are harder to read and may indicate a need to extract sub-components. Threshold: **5 levels deep**.

**Detection:** A JSX element whose JSX child tree exceeds 5 levels of nesting.

**Action:** Extract inner JSX into a named sub-component.

---

### `REACT_UNOPTIMIZED_LIST` — List rendering without key or large list

**Severity:** Warning (flag)  
**Files:** `.tsx`

Missing `key` props in list rendering causes React reconciliation issues. Lists over 50 items may need virtualization.

**Detection:**
- `map()` producing JSX without a `key` attribute on the root element
- List size exceeding 50 items

**Correct**
```typescript
return (
  <ul>
    {items.map((item) => (
      <li key={item.id}>{item.name}</li>
    ))}
  </ul>
);
```

**Wrong**
```typescript
return (
  <ul>
    {items.map((item) => (
      <li>{item.name}</li> // missing key
    ))}
  </ul>
);
```

---

### `REACT_COMPLEX_STATE` — useState with complex object

**Severity:** Warning (flag)  
**Files:** `.tsx`

`useState` initialized with an object having more than 5 properties suggests a need for `useReducer`. Threshold: **5 properties**.

**Detection:** `useState(obj)` where `obj` is an object literal with > 5 properties.

**Action:** Suggest converting to `useReducer` for complex state.

---

### `REACT_NESTED_UPDATE` — Nested state updates

**Severity:** Warning (flag)  
**Files:** `.tsx`

Calling a state setter from within another state setter's callback can cause unexpected re-renders.

**Detection:** A `set*` call whose ancestor is another `set*` call expression.

---

## Validation Output Format

When reporting issues after code review, group by file and severity:

```
### Static Analysis Results

**Violations (must fix before completing Step 3):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
  Fix: <suggested fix>

**Warnings (review with user):**
- [RULE_ID] File: ComponentName.tsx, Line ~N — <message>
  Suggestion: <improvement>

✅ No violations found. N warnings flagged.
```

Do not proceed with Step 4 (Documentation) until all violations are resolved.
