# @cimplify/sdk

The official TypeScript SDK for building storefronts with the Cimplify Commerce Engine. Typed `fetch` client + 90+ React components + Server-Components helpers + an in-process mock that mirrors the production API.

## Where to start

| Goal | Read |
|---|---|
| Scaffold a storefront and ship it | [Storefronts guide](../../docs/sdk/storefronts.md) |
| Architectural rules + agent contract | [`AGENTS.md`](./AGENTS.md) |
| Step-by-step task walkthroughs | [`RECIPES.md`](./RECIPES.md) |
| SDK ↔ backend schema contract | [`CONTRACTS.md`](./CONTRACTS.md) |
| Pricing flow internals | [`./sdk-pricing-flow.md`](./sdk-pricing-flow.md) |

## Installation

```bash
bun add @cimplify/sdk          # bun (recommended)
npm install @cimplify/sdk
yarn add @cimplify/sdk
```

## Public exports

| Subpath | Use |
|---|---|
| `@cimplify/sdk` | Browser-safe typed client (`createCimplifyClient`), domain types, `Result<T, E>` |
| `@cimplify/sdk/react` | 90+ components + 30+ hooks (`useCart`, `useProducts`, `<CataloguePage>`, `<CartDrawer>`, …) |
| `@cimplify/sdk/server` | Server Components / Server Actions: `getServerClient()`, `tags.*`, `revalidate*` |
| `@cimplify/sdk/utils` | Money helpers, formatting, slug helpers |
| `@cimplify/sdk/advanced` | Lower-level escape hatches |
| `@cimplify/sdk/lint` | Declarative lint authoring types + the bundled starter rule set |
| `@cimplify/sdk/lint-rules` | Static, version-matched JSON rules consumed by `cimplify lint` |
| `@cimplify/sdk/mock` | Programmatic mock for tests / MSW |
| `@cimplify/sdk/mock/msw` | MSW handlers wrapping the in-process mock |
| `@cimplify/sdk/testing` | Test harness — zod schemas, `createTestClient`, fixtures, `assertX` helpers |
| `@cimplify/sdk/testing/suite` | Pre-baked vitest suites: `createBrandSuite`, `createCartFlowSuite`, `createContractSuite` |
| `@cimplify/sdk/styles.css` | Pre-compiled Tailwind utilities for the React components |

`@cimplify/sdk/lint` is intentionally parser-free: it exports serializable rule declarations and six reusable matching primitives. The runner owns AST parsing and reads the installed SDK's `lint-rules` JSON as bounded, validated data—project JavaScript is never loaded by a lint pass. Older SDKs fall back to the trusted rule set bundled with the CLI.

### SDK correctness rules

Rules are declarations, not AST visitors. The v1 vocabulary is:

- `banned-import`
- `banned-jsx-element`
- `banned-call-argument`
- `banned-classname-pattern`
- `required-result-check`
- `required-wrapper-call`

Add a rule with `defineLintRule(...)`, include it in `starterLintRuleSet`, and add one clean plus one violating fixture under the CLI's `src/lint/__fixtures__` directory. The fixture test runs every rule independently, so a new declaration cannot silently rely on another rule's diagnostic. Flow-sensitive rules that do not fit these primitives belong in a later semantic pass rather than a one-off walker.

## Mock storefront API

This package ships an embeddable mock storefront API used by tests and local dev:

```bash
bunx @cimplify/sdk mock --seed <default|retail|fashion|…>   # standalone HTTP server on :8787
```

Or import it programmatically via `@cimplify/sdk/mock` / `@cimplify/sdk/mock/msw`.

## Component registry

`@cimplify/sdk` ships a registry of ejectable components at `./registry/*.json` (60+ entries) — same shadcn-style pattern: `cimplify add product-grid` copies the component source into your project so you can edit it, with imports already rewritten to `@cimplify/sdk` / `@cimplify/sdk/react`.

The registry data lives here; the `add` / `list` commands live in [`@cimplify/cli`](../cli/README.md).

## CLI moved

Everything else — `init`, `login`, `projects`, `link`, `deploy`, `env`, `domains`, `logs`, etc. — lives in the dedicated CLI package:

```bash
npm i -g @cimplify/cli      # one-time install
cimplify --help             # full surface

# Or zero-install:
bunx @cimplify/cli deploy
```

See [`@cimplify/cli`](../cli/README.md) for the full subcommand list.

## Quick start

```ts
import { createCimplifyClient } from "@cimplify/sdk";

const client = createCimplifyClient({
  publicKey: process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY,
});

// Catalogue
const products = await client.catalogue.getProducts({ limit: 24 });
if (!products.ok) throw products.error;

const single = await client.catalogue.getProductBySlug("studio-tee-natural");
const variants = await client.catalogue.getVariants("prod_xyz");

// Cart — flat add-to-cart payload (variant + add-ons go on the body, not nested)
await client.cart.addItem({
  item_id: "prod_studio-tee-natural",
  quantity: 1,
  variant_id: "var_studio-tee-natural_size_s",
  add_on_options: ["addopt_gift_wrap"],
  special_instructions: "Please ship without invoice.",
});

const cart = await client.cart.get();
await client.cart.updateQuantity("ci_…", 2);
await client.cart.removeItem("ci_…");
await client.cart.applyCoupon("WELCOME10");

// Auth — single contact arg, then `(contact, code)` to verify
await client.auth.requestOtp("+233244000000");
await client.auth.verifyOtp("+233244000000", "123456");
await client.auth.logout();
```

Every call returns `Result<T, CimplifyError>`. Always check `.ok`:

```ts
const r = await client.catalogue.getProducts();
if (!r.ok) {
  console.error(r.error.code, r.error.message);
  return;
}
console.log(r.value.items);
```

## React components

```tsx
"use client";
import {
  CimplifyProvider,
  CartDrawerProvider,
  CartDrawer,
  CataloguePage,
  ProductPage,
  CartPage,
  CheckoutPage,
  AccountDashboardPage,
  AccountOrdersPage,
  useCart,
  useCartDrawer,
} from "@cimplify/sdk/react";

function Root({ children }) {
  return (
    <CimplifyProvider client={client}>
      <CartDrawerProvider>
        {children}
        <CartDrawer onCheckout={() => router.push("/checkout")} />
      </CartDrawerProvider>
    </CimplifyProvider>
  );
}
```

90+ components ship — variant-aware product cards, customizers (variant / add-on / bundle / composite / billing-plan / customer-input / scheduling), full pages (`<CataloguePage>`, `<ProductPage>`, `<CartPage>`, `<CheckoutPage>`, `<SearchPage>`, `<AccountDashboardPage>`, `<AccountOrdersPage>`), cart drawer with free-shipping progress, primitives (`<Price>`, `<QuantitySelector>`, `<DealBanner>`, `<DeliveryEstimate>`, `<ChatWidget>`).

## Server Components (Next.js App Router)

```ts
import { getServerClient, tags, revalidateProducts } from "@cimplify/sdk/server";
import { unstable_cacheTag as cacheTag, unstable_cacheLife as cacheLife } from "next/cache";

async function getCatalogue() {
  "use cache";
  cacheTag(tags.products());
  cacheLife("hours");
  const r = await getServerClient().catalogue.getProducts({ limit: 24 });
  if (!r.ok) throw new Error(r.error.message);
  return r.value.items;
}

// In a Server Action after a mutation:
"use server";
async function refreshProducts() {
  await revalidateProducts();
}
```

## Testing harness

Every scaffolded storefront ships with a 30-second feedback loop:

```bash
bun run check              # typecheck + lint + brand + cart-flow + contract suites
bun run check:brand        # schema invariants on lib/brand.ts
bun run check:cart         # add → dedupe → remove against in-process mock
bun run check:contract     # validates payload + response shapes
bun run check:visual       # Playwright snapshots (fashion ref template)
```

Custom test cases use the `extend` hook on the suites:

```ts
import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";
import { brand } from "../lib/brand";
import { expect } from "vitest";

createCartFlowSuite({
  seed: brand.mock.seed,
  businessId: brand.mock.businessId,
  extend: ({ getHandle, it }) => {
    it("every product priced in GHS", async () => {
      const list = await getHandle().client.catalogue.getProducts();
      if (!list.ok) throw list.error;
      for (const p of list.value.items ?? []) expect(p.currency).toBe("GHS");
    });
  },
});
```

The mock is the oracle. `createTestClient({ seed })` boots the same Hono mock you run locally, in-process, via the SDK's first-class `fetch` injection — parallel-safe, no global mutation. See `RECIPES.md` for the full workflow.

## Money

All monetary values use the branded `Money` type — string at runtime, distinct from plain `string` at compile time.

```ts
import { money, formatPrice, isOnSale, getDiscountPercentage, type Money } from "@cimplify/sdk";

const price = money("29.99");
parseFloat(price); // 29.99

formatPrice(29.99, "GHS"); // "GH₵29.99"

if (isOnSale(product)) console.log(`${getDiscountPercentage(product)}% off`);
```

## Error handling

```ts
import { CimplifyError } from "@cimplify/sdk";

const r = await client.cart.addItem({ item_id: "invalid", quantity: 1 });
if (!r.ok && r.error instanceof CimplifyError) {
  console.error(r.error.code, r.error.message, r.error.retryable);
}
```

For schema-shape errors (in tests, contract validation), `assertX` helpers throw a typed `SchemaViolationError` with structured `issues[]` and an RFC 7807 `toJSON()`:

```ts
import { assertCart, SchemaViolationError } from "@cimplify/sdk/testing";

try {
  assertCart(response);
} catch (e) {
  if (e instanceof SchemaViolationError) console.error(e.toJSON());
}
```

## TypeScript

Domain types are exported from the root:

```ts
import type {
  Product, ProductWithDetails, ProductVariant,
  Category, Collection, Bundle, Composite,
  Cart, CartItem, Order, Customer,
} from "@cimplify/sdk";

// Schema-derived types from the testing entry:
import type { Brand, AddItemPayload, CheckoutResponse } from "@cimplify/sdk/testing";
```

## License

MIT
