React

Hooks with TanStack Query, and a cart.

With TanStack Query the catalogue is cached, refetched in the background and shared between components.

npm install @tanstack/react-query
src/cashiar.ts
import { useMutation, useQuery } from '@tanstack/react-query';

const BASE = 'https://api.cashiar.app/v1/storefront';
const KEY = import.meta.env.VITE_CASHIAR_KEY; // pk_live_…

async function call<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${KEY}`, ...init?.headers },
  });
  const data = await res.json();
  if (!res.ok) throw new Error(data.message);
  return data;
}

export const useShop = () =>
  useQuery({ queryKey: ['shop'], queryFn: () => call<Shop>('/shop'), staleTime: 5 * 60_000 });

export const useProducts = (collection?: string) =>
  useQuery({
    queryKey: ['products', collection],
    queryFn: () =>
      call<{ items: Product[]; nextCursor: string | null }>(
        `/products${collection ? `?collection=${collection}` : ''}`,
      ),
    staleTime: 60_000,
  });

export const usePlaceOrder = () =>
  useMutation({
    mutationFn: ({ order, idempotencyKey }: { order: Order; idempotencyKey: string }) =>
      call<PlacedOrder>('/orders', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
        body: JSON.stringify(order),
      }),
  });
src/Checkout.tsx
import { useState } from 'react';
import { usePlaceOrder } from './cashiar';

export function Checkout({ lines }: { lines: CartLine[] }) {
  // One key per checkout: a retry after a dropped connection never orders twice.
  const [idempotencyKey] = useState(() => crypto.randomUUID());
  const place = usePlaceOrder();
  const submit = (form: FormData) =>
    place.mutate(
      {
        idempotencyKey,
        order: {
          customerName: String(form.get('name')),
          customerPhone: String(form.get('phone')),
          fulfilment: 'pickup',
          payNow: true,
          lines,
        },
      },
      { onSuccess: (placed) => placed.payUrl && window.location.assign(placed.payUrl) },
    );
  return (
    <form action={submit}>
      <input name="name" placeholder="Your name" required />
      <input name="phone" placeholder="024 000 0000" required />
      <button disabled={place.isPending}>Pay now</button>
      {place.error && <p role="alert">{place.error.message}</p>}
    </form>
  );
}

Types for Shop, Product, Order and PlacedOrder can be generated from the OpenAPI document with a tool such as openapi-typescript:

npx openapi-typescript https://docs.cashiar.app/openapi/storefront.json -o src/cashiar-api.d.ts