# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`@netfoundry/myzrok-extension` is a **zrok UI extension** — a plugin that plugs into the host zrok UI's
extension system (see `src/models/extension.ts`). It is built as a library (peer deps on `react`,
`@mui/material`, `@xyflow/react`, `axios`, `react-router`, etc. — none of these are bundled), not a
standalone app, so it cannot be run/served on its own.

The extension adds a Billing feature to zrok: a `/billing` route (`MyzrokDashboard`) and a "Billing" nav
item, backed by Chargebee for payment collection and a backend API called "myzrok-orchestration" for
billing accounts, plans, subscriptions, and invoices.

`src/index.tsx` is the entry point and exports the extension's `default manifest: ExtensionManifest`. This
manifest wires up:
- `routes` / `navItems` — the `/billing` page and its nav icon.
- `headScripts` / `bodyScripts` — injects and initializes the Chargebee JS SDK.
- `initialState` (`MyzrokExtensionState`) — the extension's namespaced slice of the host's store
  (`billingAccount`, `billingProvider`, `plans`, `billingInformation`, `billingDetails`, `cardDetails`,
  `accordionState`, `subscription`).
- `onUserLogin` / `onUserLogout` — lifecycle hooks fired by the host; login fetches plans and looks up
  (or notes the absence of) a billing account for the user's email.

## Build

```bash
npm run build   # rm -rf dist; rollup -c (JS bundle); rollup -c ./rollup.d.config.js (type declarations)
npm run clean   # rm -rf dist
```

There is no dev server, lint script, or test suite in this repo — `build` is the only defined script.
The build produces `dist/bundle.min.js` (ES module, via `rollup.config.mjs`) and `dist/index.d.ts`
(bundled type declarations, via `rollup.d.config.js`, which rolls up `dist/dts/index.d.ts` produced by
`tsc`'s `emitDeclarationOnly` pass declared in `tsconfig.json`).

Peer dependencies (`@mui/material`, `react`, `@xyflow/react`, `axios`, `react-router`, etc.) are listed as
`external` in `rollup.config.mjs` and must not be bundled — the host app supplies them.

## Architecture

### Extension context, not local React state ownership

Components generally do **not** own top-level billing state in local `useState` as the source of truth —
they read it from the host's store via `context: ExtensionContext` (`src/models/extension.ts`) and mirror
it into local state for rendering:

- `context.getState<MyzrokExtensionState>()` — read the extension's current state slice.
- `context.setState<MyzrokExtensionState>(partial)` — shallow-merge an update into it.
- `context.subscribe<MyzrokExtensionState>(selector, callback)` — react to a field changing; **every**
  subscription's unsubscribe function must be collected and called in a `useEffect` cleanup (see the
  pattern in `MyzrokDashboard.tsx` and `BillingSummary.tsx`), otherwise the extension leaks listeners
  across route mounts.

`MyzrokExtensionState` (defined in `src/index.tsx`) is the single shape for this extension's slice of
host state. When adding a new piece of cross-component state, add it here, subscribe to it where it's
read, and write to it via `context.setState` — don't invent a parallel prop-drilling path.

`ExtensionComponentProps` (`src/models/component-props.ts`) is the common prop shape passed down into
billing components (`user`, `context`, `billingAccountId`, `billingInformation`, `billingDetails`,
`cardDetails`, `plans`, `planMap`, `billingProviderId`). Most components under `src/components/` extend
or destructure from this rather than defining ad hoc prop sets.

### Data flow: MyzrokDashboard is the orchestrator

`src/pages/MyzrokDashboard.tsx` is the top of the billing feature tree. It:
1. Subscribes to every relevant slice of `MyzrokExtensionState`.
2. Decides `hasSubscription` from `billingDetails.planId` + `billingDetails.subscriptionId`.
3. Renders `InitialSubscriptionFlow` (`src/components/NewSubscriptionFlow.tsx`) when the user has no
   active subscription yet, or `BillingSummary` otherwise — these two are mutually exclusive views of the
   same underlying account.
4. Owns `setBillingContext`/`setBillingAndSubscriptionContext`, which translate a raw `BillingAccount`
   (+ optionally a `Subscription`) from the API into the extension's `billingInformation`,
   `billingDetails`, and `cardDetails` state shapes. Any change to what fields the backend returns on
   `BillingAccount`/`Subscription` that should surface in the UI needs a corresponding update here.

### Generated API client (`src/api/`) — do not hand-edit

`src/api/` (`apis/`, `models/`, `docs/`, `runtime.ts`) is generated by **openapi-generator** (see
`openapitools.json`, generator version pinned there; TS fetch-api generator based on `src/runtime.ts`'s
header). It is checked into the repo but should be regenerated from the myzrok-orchestration OpenAPI spec
rather than edited by hand — `src/api/.openapi-generator-ignore` lists any files exempted from
regeneration. `src/runtime.ts` at the repo root is a duplicate/older copy of `src/api/runtime.ts`; the one
actually used by generated code is `src/api/runtime.ts`.

API classes (`BillingAccountApi`, `PlanApi`, `SubscriptionApi`, `BillingProviderApi`, `InvoicesApi`,
`BillingHooksApi`) are plain classes instantiated directly (`new SubscriptionApi()`), not injected —
follow that convention when calling the API from a new component rather than introducing a client/context
provider for it.

`src/api/validators/` (currently `BillingAccountValidator.ts`) holds hand-written Joi schemas layered on
top of the generated request models, used for client-side form validation (name/phone/email/address
patterns, message strings for each Joi rule). Extend these — not the generated models — when adding
validation for a new field.

### Models split: `src/models/` vs `src/api/models/`

- `src/api/models/` — generated wire-format types matching the myzrok-orchestration API responses
  (`BillingAccount`, `Plan`, `Subscription`, etc.).
- `src/models/` — hand-written UI/domain types layered on top: `billing.ts` (`BillingInformation`,
  `BillingDetails`, `CardDetails`, `CardBrand` enum — the shapes actually stored in
  `MyzrokExtensionState`), `billingProvider.ts`, `user.ts`, `component-props.ts`, and `extension.ts` (the
  contract with the host zrok UI: `ExtensionManifest`, `ExtensionContext`, `ExtensionRoute`,
  `ExtensionNavItem`, `SLOTS`, etc.).

When converting an API response into UI state (as `MyzrokDashboard.setBillingAndSubscriptionContext`
does), map explicitly field-by-field rather than passing the API model straight into UI state — the two
model sets are intentionally decoupled.

### Chargebee integration

Chargebee is loaded and initialized via the `headScripts`/`bodyScripts` mechanism in `src/index.tsx`
(site + publishable key are currently hardcoded there as `nfdev-test` — check before assuming this is
production config). `@chargebee/chargebee-js-react-wrapper` is used inside payment components
(`PaymentInformation.tsx`) for the hosted card fields. `CardBrand` in `src/models/billing.ts` is matched
against Chargebee's brand strings when converting card info from `BillingAccount.cardInformation`.

### Styling

Global CSS lives in `src/assets/styles/` (`chargebee.css`, `myzrok.css`) and is imported once from
`src/index.tsx`; MUI (`@mui/material`, `@mui/icons-material`, `@mui/x-data-grid`) is the component library
throughout, generally styled via the `sx` prop rather than separate stylesheets. SVGs are imported as
React components via `@svgr/rollup` (`svgr.config.json`).
