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… | Client | Hook |
|---|---|---|
| List delivery methods | storefront.delivery.methods() | useDeliveryMethods() |
| Quote a fee at the customer's location | storefront.delivery.quote() | useDeliveryQuote() |
| Capture the customer's address + coordinates | — | useAddress() + <TAddress> |
| List payment methods | storefront.payments.methods() | usePaymentMethods() |
| List products / services | storefront.catalog.list() / .services() | useProducts() / useProducts({type:'service'}) |
| Book a service | storefront.bookings.create() | useBooking() |
| Validate a coupon | storefront.coupons.validate() | useCoupon() |
| Place the order | storefront.checkout() | useCheckout() |
| Track events + UTM (pixels + ThreeU) | createTracker() | useTracker() |
| Capture a lead | storefront.leads.create() | useLead() |
| Blog / FAQ / careers / comparison / legal | storefront.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.
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.
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.
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.
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.
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.
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.
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.
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.
// 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.
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.
| Content | Client | React |
|---|---|---|
| Blog list / post | content.blog() / content.blogPost(slug) | useBlogPost(slug) |
| FAQs | content.faqs(category?) | useFaqs(category?) |
| Careers | content.careers() / content.career(slug) | — |
| Comparison | content.comparisons() / content.comparison(slug) | — |
| Legal | content.legal() / content.legalDoc(slug) | — |
Page <meta>/JSON-LD | content.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.
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();