3U

Commerce: Delivery, Payments, Address, Checkout & Tracking

Everything a theme needs to sell: brand delivery methods with location-based fees, the TAddress field, payment methods, products & services, clean checkout with coupons, and an events tracker.

Overview

A storefront theme reads the brand's live commerce data through the Public Storefront client and its theme hooks — no secret token, brand resolved from the store's platform-id. Everything below is exposed on ThreeuStorefront (framework-agnostic) and as React hooks from threeu-sdk/theme.

You want to…ClientHook
List delivery methodsstorefront.delivery.methods()useDeliveryMethods()
Quote a fee at the customer's locationstorefront.delivery.quote()useDeliveryQuote()
Capture the customer's address + coordinatesuseAddress() + <TAddress>
List payment methodsstorefront.payments.methods()usePaymentMethods()
List products / servicesstorefront.catalog.list() / .services()useProducts() / useProducts({type:'service'})
Book a servicestorefront.bookings.create()useBooking()
Validate a couponstorefront.coupons.validate()useCoupon()
Place the orderstorefront.checkout()useCheckout()
Track events + UTM (pixels + ThreeU)createTracker()useTracker()
Capture a leadstorefront.leads.create()useLead()
Blog / FAQ / careers / comparison / legalstorefront.content.*useFaqs() / useBlogPost()

Wiring the Provider

Give the theme a storefront client and (optionally) analytics config once, at the root. pixels come from storefront.getConfig().pixels.

Root provider
TSX
import { ThreeuThemeProvider } from "threeu-sdk/theme";

<ThreeuThemeProvider
  brand={{ id: "my-store", name: "Abaya Store" }}
  publicToken={import.meta.env.VITE_THREEU_PUBLIC_KEY}
  locale="ar"
  currency="QAR"
  analytics={{
    pixels: config.pixels,          // { meta, tiktok, snapchat, google }
    apiBaseUrl: "https://api.threeu.app",
  }}
>
  <App />
</ThreeuThemeProvider>

1. Delivery Methods + Location-Based Fee

useDeliveryMethods() returns the brand's enabled methods (store pickup, ThreeU driver, and any carrier plugins). Each method carries a pricing rule (free / fixed / distance / provider) and a flat price when one applies. For distance pricing the fee depends on how far the customer is from the store, so you ask for a live quote with their coordinates — the fee is always recomputed server-side (a tampered client total is rejected at checkout).

Provider-enforced carriers

When method.provider_enforced is true (e.g. Aramex), the carrier prices the shipment itself — fee comes back null with a determined-by-provider message. Show that instead of a number.

Delivery methods + live quote
TSX
import { useDeliveryMethods, useDeliveryQuote } from "threeu-sdk/theme";

function DeliveryPicker({ address, subtotal, onPick }) {
  const { data: methods, loading } = useDeliveryMethods();
  const { quote, data: fee } = useDeliveryQuote();

  async function choose(method) {
    const q = await quote({ method: method.id, lat: address.lat, lng: address.lng, subtotal });
    onPick(method, q.fee, q); // q.needs_location === true → ask for the map pin
  }

  if (loading) return null;
  return methods.map((m) => (
    <button key={m.id} onClick={() => choose(m)}>
      {m.name.en} — {m.provider_enforced ? "set by carrier" : m.price ?? "calculated"}
    </button>
  ));
}

2. TAddress — Address + Coordinates in One Object

<TAddress> captures both a human-readable address string AND { lat, lng } — because the coordinates drive distance pricing. It is map-library-agnostic: pass a map render-prop to drop in your own Google/Mapbox/Leaflet picker and report picks back via setPoint. Without a map it still works as a text field plus a browser-geolocation button. useAddress() manages the state and tells you when coordinates are present.

TAddress with a map picker
TSX
import { TAddress, useAddress } from "threeu-sdk/theme";

function AddressStep({ onReady }) {
  const [address, addr] = useAddress();
  return (
    <>
      <TAddress
        value={address}
        onChange={addr.set}
        map={({ lat, lng, setPoint }) => (
          <MyMap lat={lat} lng={lng} onPick={(la, ln) => setPoint(la, ln)} />
        )}
      />
      <button disabled={!addr.hasCoordinates} onClick={() => onReady(address)}>Continue</button>
    </>
  );
}

// address === { lat, lng, address, city?, area?, building?, floor?, notes? }

3. Payment Methods

usePaymentMethods() returns the brand's enabled receive-side methods: Cash on Delivery plus every active payment plugin (type is COD, PG, or BNPL). Each carries a fee and a logo. Use the method's id as the paymentMethod you pass to checkout.

Payment methods
TSX
import { usePaymentMethods } from "threeu-sdk/theme";

function PaymentPicker({ value, onChange }) {
  const { data: methods } = usePaymentMethods();
  return (methods ?? []).map((m) => (
    <label key={m.id}>
      <input type="radio" name="pay" value={m.id} checked={value === m.id} onChange={() => onChange(m.id)} />
      {m.logo && <img src={m.logo} alt="" />}
      {m.name} {m.fee > 0 && `(+${m.fee})`}
    </label>
  ));
}

4. Products & Services

useProducts() lists the catalog as normalized StorefrontProduct models. A service is a product the merchant flagged as bookable; filter to services with useProducts({ type: 'service' }) (or storefront.catalog.services()) and to physical goods with type: 'goods' — the storefront endpoint filters server-side and the normalized product.type is 'service' | 'product'. Read the brand's offers_goods / offers_services flags from getConfig() to know which a store sells. The same TProductGrid / TProductCard render either; see the Headless page for the full useProduct() controller.

Products / services
TSX
import { useProducts } from "threeu-sdk/theme";

function ServicesList() {
  const { data: services, loading } = useProducts({ type: "service" });
  if (loading) return <Spinner />;
  return <TProductGrid products={services} columns={3} />;
}

// Non-React: const { items } = await storefront.catalog.services({ limit: 8 });

5. Bookings (Services)

For service businesses, useBooking() books a service — it creates a booking order (no shipping) for the chosen date/time. Note the current shape: date and time are free-form strings and there is no availability/slots API yet, so present your own date/time UI and validate availability out of band.

No slots API yet

Bookings record an order with the date/time you send; there is no server-side calendar or capacity check. Build your own availability UI; slots are on the roadmap.

Book a service
TSX
import { useBooking } from "threeu-sdk/theme";

function BookServiceButton({ serviceSlug, date, time, form }) {
  const { book, submitting } = useBooking();
  return (
    <button disabled={submitting}
      onClick={async () => {
        const res = await book({ serviceSlug, date, time, form });
        window.location.href = `/orders/${res.orderId}`;
      }}>
      Book
    </button>
  );
}

// Non-React: await storefront.bookings.create({ serviceSlug, date, time, form })

6. Checkout with Coupons

useCoupon() validates a code for the current subtotal — a rejected coupon resolves to { valid:false, reason } (unknown / inactive / expired / exhausted) rather than throwing, so you can show the reason inline. useCheckout() then places the order; the server independently re-validates and applies the discount, recomputes the delivery fee, and rejects a tampered total. Pass the coupon code and the customer coordinates and you are done.

Never trust client totals

The backend recomputes the delivery fee (from the coordinates) and the coupon discount, and rejects the order if the client total matches none of the authoritative combinations. Always send lat/lng so the fee is right.

Coupon + checkout
TSX
import { useCoupon, useCheckout } from "threeu-sdk/theme";

function Checkout({ items, address, subtotal, deliveryMethod, paymentMethod }) {
  const { apply, coupon } = useCoupon();
  const { checkout, submitting } = useCheckout();

  async function placeOrder() {
    const discount = coupon?.valid ? coupon.discount_amount ?? 0 : 0;
    const result = await checkout({
      items,                       // [{ productId, quantity, price }]
      shipping: {
        fullName: address.name, phone: address.phone, email: address.email,
        address: address.address, city: address.city, area: address.area,
        lat: address.lat, lng: address.lng, // → server distance fee
      },
      deliveryMethod,
      paymentMethod,
      discountCode: coupon?.valid ? coupon.coupon?.code : undefined,
      totalAmount: subtotal - discount, // server figure is authoritative
      currency: "QAR",
    });
    window.location.href = `/orders/${result.orderId}`;
  }

  return <button disabled={submitting} onClick={placeOrder}>Place order</button>;
}

7. Events & SEO Tracking

useTracker() builds a tracker wired to the brand's configured pixels (Meta, TikTok, Snapchat, Google) and to ThreeU telemetry — fire an event once and it lands everywhere. It is SSR-safe and never throws (a broken pixel cannot break checkout). Pixels fire only when the brand configured that pixel id and its script is loaded; ThreeU telemetry receives summary scalars only — never PII.

The 15 standard events (GA4-aligned): page_view, view_item, view_item_list, select_item, search, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_shipping_info, add_payment_info, add_coupon, purchase, sign_up, login. The engagement/SEO signals — search, view_item, view_item_list — are how you feed search and product-discovery analytics.

Tracking commerce & SEO events
TSX
import { useTracker } from "threeu-sdk/theme";
import { useEffect } from "react";

function ProductPage({ product }) {
  const track = useTracker();
  useEffect(() => {
    track.viewItem({ id: product.id, name: product.name, price: product.price });
  }, [product.id]);
  return <AddToCartButton onClick={() =>
    track.addToCart({ id: product.id, price: product.price, quantity: 1 })
  } />;
}

// track.search("abaya"); track.purchase({ orderId, value, currency: "QAR", items });
// Outside React: import { createTracker } from "threeu-sdk/analytics";

8. Marketing Campaign Links (UTM Attribution)

The tracker auto-captures `utm_*` params from the landing URL and rides them on every ThreeU event, so a purchase is attributed to the campaign/ad that drove the visit — no extra code. ThreeU decorates ad destination URLs with utm_source/medium/campaign/content (the campaign/ad id is base36-encoded in utm_campaign/utm_content).

For first-touch landing attribution independent of a commerce event, the platform also exposes a public pixel endpoint that decodes the campaign scheme and records a MarketingAttribution row: POST /api/analytics/pixel/events (send platform-id + the event name + the page URL). Use it to log a page_view/lead landing before any purchase happens.

Who fires what

The SDK tracker emits the storefront:* commerce/SEO events + UTM. Campaign / ad / lead:captured / conversion:attributed events are produced by ThreeU's backend integrations (ads sync, the pixel endpoint, the leads API) — not by the theme.

Campaign attribution
TSX
// The tracker already forwards UTM automatically:
const track = useTracker(); // reads window.location utm_* on init
track.purchase({ orderId, value: 240, currency: "QAR" }); // ← carries utm_*

// First-touch landing attribution (public, no auth):
await fetch(`${API}/api/analytics/pixel/events`, {
  method: "POST",
  headers: { "Content-Type": "application/json", "platform-id": brandId },
  body: JSON.stringify({ event: "page_view", url: window.location.href }),
});

9. Lead Capture

useLead() submits a storefront contact / enquiry / book-a-call form. It is public (guest, brand-scoped) and creates a brand-scoped Lead + attribution row and fires lead:captured. Pass utm (or let the tracker's captured UTM through) so the lead is attributed to its campaign. A phone or email is required.

Capture a lead
TSX
import { useLead } from "threeu-sdk/theme";

function ContactForm() {
  const { submit, submitting, result } = useLead();
  if (result?.success) return <p>Thanks — we will be in touch.</p>;
  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      const f = new FormData(e.currentTarget);
      await submit({
        name: f.get("name"), phone: f.get("phone"), email: f.get("email"),
        message: f.get("message"), source: "contact_form",
      });
    }}>
      {/* inputs… */}
      <button disabled={submitting}>Send</button>
    </form>
  );
}

// Non-React: await storefront.leads.create({ phone, email, message, utm })

10. Content: Blog, FAQ, Careers, Comparison, Legal

The brand's public content — blog posts, FAQs, careers, comparison pages, legal docs — is served by the platform's public content engine and exposed on the storefront client as storefront.content.* (scoped to the brand). Read-only, public. React helpers: useFaqs(category?) and useBlogPost(slug); the rest are on the client.

ContentClientReact
Blog list / postcontent.blog() / content.blogPost(slug)useBlogPost(slug)
FAQscontent.faqs(category?)useFaqs(category?)
Careerscontent.careers() / content.career(slug)
Comparisoncontent.comparisons() / content.comparison(slug)
Legalcontent.legal() / content.legalDoc(slug)
Page <meta>/JSON-LDcontent.meta(path)

Pair FAQ/blog content with the SDK's JSON-LD helpers (faqJsonLd, and the SEO page) so the pages are crawlable.

Content is optional per brand

These records only exist if the brand (or ThreeU) published them via the content engine. Empty responses are normal — render nothing when a section has no entries.

Render brand FAQ / blog
TSX
import { useFaqs } from "threeu-sdk/theme";
import { TJsonLd, faqJsonLd } from "threeu-sdk/theme";

function FaqPage() {
  const { data: faqs = [] } = useFaqs();
  const items = faqs.map((f) => ({ question: f.title, answer: f.value?.answer }));
  return (
    <>
      <TJsonLd schema={faqJsonLd(items)} />
      {faqs.map((f) => <details key={f.slug ?? f.title}><summary>{f.title}</summary><div>{f.value?.answer}</div></details>)}
    </>
  );
}

// Non-React: const posts = await storefront.content.blog();