# Products and collections from Commerce Orbit

> How Product, Products and Collection fields and the Commerce Orbit blocks are delivered, and how to render them.

Source: https://docs.bifrostorbit.com/cms/developers/commerce-blocks/

A space connected to Commerce Orbit under **Configuration › Orbit connections** has three more field types and four blocks. The entry stores only what was picked, `{ "id", "name" }`. Every answer from the delivery API, the SDK and GraphQL fills in the rest from Commerce Orbit's Storefront API, kept for at most a minute, and fresh in preview.

## A product

```json
"product": {
  "id": "6b0000000000000000000001",
  "slug": "fjell-gtx-boot",
  "name": "Fjell GTX boot",
  "summary": "A waterproof leather boot for long days on rough ground.",
  "image": { "url": "https://cdn.northwilds.com/fjell-gtx.webp", "alt": "Fjell GTX boot", "mimeType": "image/webp" },
  "images": [],
  "price": 18900,
  "priceMax": 18900,
  "compareAtPrice": null,
  "currency": "EUR",
  "available": true,
  "inStock": true,
  "stock": 14,
  "variants": [{ "id": "v-42", "title": "EU 42", "options": ["42"], "sku": "FJELL-42", "price": 18900, "compareAtPrice": null, "available": true, "quantity": 3 }],
  "url": "https://app.bifrostorbit.com/commerce/shop/northwilds/products/fjell-gtx-boot",
  "addToCartUrl": "https://app.bifrostorbit.com/commerce/shop/northwilds/cart?add=v-42"
}
```

- **Prices** are in minor units, such as cents: `18900` is €189. `price` is the lowest variant price and `priceMax` the highest.
- **`currency`** is the page's region's currency when the shop sells in it, and the shop's own currency otherwise.
- **`available: false`** means the product is not on sale now, or Commerce Orbit could not be reached and had never answered. The picked name is kept so the page still reads well; hide the price and the button.
- **`inStock`** says whether any variant can be bought now. **`stock`** is the total left when every variant counts its stock, and `null` otherwise. Each variant has its own `available` and `quantity`.
- **`addToCartUrl`** adds the first variant that can be bought to the cart on the hosted shop, and is `null` when the product is sold out. To build your own cart, use the variant ids with Commerce Orbit's Storefront API.

## A collection

A Collection field arrives as `{ id, slug, name, description, image, productCount, url, available, products }`, with up to 24 of its products in the shape above.

## The blocks

| Block | Key | Fields it arrives with |
| --- | --- | --- |
| Product card | `commerceProductCard` | `product`, `heading`, `buttonLabel` |
| Product grid | `commerceProductGrid` | `heading`, `intro`, `products`, `collection`, `limit`, and `items`: the picked products, or the collection's products when none are picked, up to `limit` |
| Add to cart button | `commerceAddToCart` | `label`, `product` |
| Collection list | `commerceCollectionList` | `heading`, `intro`, `limit`, and `collections`: every collection in the shop, without their products |

Render the grid from `items`, so your component never has to choose between the two sources:

```tsx
import { commercePrice, type CmsCommerceProduct } from '@bifrost-orbit-cms/sdk'
import type { BlockProps } from '@bifrost-orbit-cms/sdk/react'

export function ProductGrid({ block }: BlockProps<'commerceProductGrid'>) {
  const items = (block.items ?? []) as CmsCommerceProduct[]
  return (
    <section data-cms-id={block.id}>
      {block.heading ? <h2>{block.heading}</h2> : null}
      <ul>
        {items.filter((product) => product.available).map((product) => (
          <li key={product.id}>
            <a href={product.url ?? '#'}>{product.name}</a> {commercePrice(product)}
            {product.inStock ? null : ' Sold out'}
          </li>
        ))}
      </ul>
    </section>
  )
}
```

## Refreshing pages

When a product is published, taken off sale, changed or its stock changes in Commerce Orbit, CMS Orbit sends `content.published` with `reason: "commerce"` for every live page that shows it, so `createRevalidateHandler` refreshes those pages. A change to a collection refreshes the pages that show it and those with a Collection list. Pages with a Collection field also refresh when any product changes, since the product may be in the collection.

## Prices for signed-in trade customers

Delivered pages carry the shop's normal prices, the same for every visitor, so they can be cached. A trade customer signed in to the shop has prices of their own, from their price list or live from their MAM account. Lay those over the page in the visitor's browser, after it has loaded:

```tsx
'use client'
import { commercePrice, type CmsCommerceProduct, type CommercePersonalPrices } from '@bifrost-orbit-cms/sdk'
import { useCommercePrices } from '@bifrost-orbit-cms/sdk/react'

const load = async (productIds: string[]): Promise<CommercePersonalPrices | null> => {
  const response = await fetch('/api/shop/prices', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ productIds }) })
  return response.ok ? (await response.json()).data : null
}

export function Prices({ items, signedIn }: { items: CmsCommerceProduct[]; signedIn: boolean }) {
  const { products } = useCommercePrices(items, signedIn ? load : null)
  return products.map((product) => <p key={product.id}>{commercePrice(product)} {product.priceSource === 'account' ? 'Your price' : null}</p>)
}
```

`/api/shop/prices` is a route on your own server that passes the call on to Commerce Orbit with the customer's token from an `httpOnly` cookie, so the token never reaches the browser. One call covers every product on the page. See [Customer accounts and personal prices](https://docs.bifrostorbit.com/commerce/developers/customer-accounts/) for signing customers in and for the route.
