Customer Account Components Intermediate 12 min read

Order History and Order Details

Build order history listing and detailed order view components with React. Display line items, shipping status, tracking information, and order summaries.

Order history is one of the most-visited pages in any customer account. Customers come here to check shipping status, find tracking numbers, or reference past purchases. Let’s build a complete order management experience—from browsing past orders to viewing detailed line items and tracking shipments.

Two Views, Two Templates

Shopify’s customer order system uses two separate templates:

  1. Order History (templates/customers/orders.liquid) — Lists all orders with summaries
  2. Order Detail (templates/customers/order.liquid) — Shows a single order in full

This separation makes sense from both a performance and UX perspective. The history page loads lightweight summaries for many orders, while the detail page loads comprehensive data for just one. Our React components mirror this architecture.

Theme Integration

templates/customers/orders.liquid (order list)
└── sections/customer-orders.liquid
└── <div id="order-history-root">
└── OrderHistory (React)
└── OrderCard (multiple)
templates/customers/order.liquid (single order)
└── sections/customer-order.liquid
└── <div id="order-detail-root">
└── OrderDetail (React)
├── OrderLineItems
├── OrderSummary
├── FulfillmentCard
└── AddressCard

What Customers Need From Order Pages

Before diving into code, consider the questions customers bring to their order history:

  • “Did my order ship?” — Fulfillment status front and center
  • “Where’s my tracking number?” — Easy access to carrier links
  • “Which order was that blue sweater in?” — Product thumbnails for recognition
  • “How much did I spend?” — Totals visible at a glance
  • “Was I charged correctly?” — Detailed breakdown with discounts

Every component we build should help answer these questions quickly.

Data Source

PropSourceLiquid Field
ordersJSON script elementcustomer.orders
orderJSON script elementorder
order.lineItemsJSON script elementorder.line_items
order.fulfillmentsJSON script elementorder.fulfillments
order.shippingAddressJSON script elementorder.shipping_address
order.billingAddressJSON script elementorder.billing_address

Order History Architecture

The history page shows order cards in a vertical list. Each card provides enough information to identify an order without clicking through:

┌──────────────────────────────────────────────────────────────────────────────┐
│ ORDER HISTORY PAGE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ OrderHistoryHeader │ │
│ │ Order History (5 orders) [← Back to Account] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ OrderList │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ OrderCard │ │ │
│ │ │ #1005 • January 5, 2026 [Paid] [Fulfilled] │ │ │
│ │ │ │ │ │
│ │ │ ┌────┐ ┌────┐ ┌────┐ │ │ │
│ │ │ │img │ │img │ │ +2 │ 4 items • $125.00 [View Order] │ │ │
│ │ │ └────┘ └────┘ └────┘ │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────────────┐ │ │
│ │ │ #1004 • December 20, 2025 [Paid] [Partial] │ │ │
│ │ │ ... │ │ │
│ │ └───────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘

The thumbnails are crucial—they help customers find orders without remembering order numbers. A “+2” indicator shows there are more items without cluttering the card.

TypeScript Interfaces

We define two order types: a lightweight OrderSummary for the history list, and a complete Order type for the detail view:

src/types/order.ts
/**
* Lightweight order data for the history list.
* Contains just enough to render an order card.
*/
interface OrderSummary {
id: number;
name: string; // "#1005"
orderNumber: number;
createdAt: string;
customerUrl: string;
financialStatus: FinancialStatus;
fulfillmentStatus: FulfillmentStatus | null;
totalPrice: string;
itemCount: number;
lineItemsPreview: LineItemPreview[];
cancelled: boolean;
cancelledAt: string | null;
}
/**
* Full order with all details for the order page.
* Extends OrderSummary with line items, addresses, fulfillments.
*/
interface Order extends OrderSummary {
email: string;
phone: string | null;
subtotalPrice: string;
totalShipping: string;
totalTax: string;
totalDiscounts: string;
lineItems: OrderLineItem[];
shippingAddress: Address | null;
billingAddress: Address | null;
fulfillments: Fulfillment[];
discountApplications: DiscountApplication[];
note: string | null;
}
interface OrderLineItem {
key: string;
title: string;
variantTitle: string | null;
sku: string;
quantity: number;
originalPrice: string;
finalPrice: string;
finalLinePrice: string;
image: string | null;
productUrl: string;
fulfillmentStatus: string | null;
discountAllocations: DiscountAllocation[];
}
interface LineItemPreview {
title: string;
image: string | null;
}
interface Fulfillment {
createdAt: string;
trackingCompany: string | null;
trackingNumber: string | null;
trackingUrl: string | null;
lineItems: FulfillmentLineItem[];
}
type FinancialStatus = 'pending' | 'authorized' | 'paid' | 'partially_paid' | 'refunded' | 'partially_refunded' | 'voided';
type FulfillmentStatus = 'fulfilled' | 'partial' | 'unfulfilled' | 'restocked';

The extends relationship lets the detail page reuse all the summary data. Notice how prices are pre-formatted strings—Liquid handles currency formatting consistently with the store’s settings.

OrderHistory Container

The history container handles the empty state check and renders either the order list or a call-to-action for new customers:

src/components/customer/OrderHistory/OrderHistory.tsx
import { OrderCard } from './OrderCard';
import { EmptyState } from './EmptyState';
import styles from './OrderHistory.module.css';
interface OrderHistoryProps {
orders: OrderSummary[];
accountUrl: string;
}
export function OrderHistory({ orders, accountUrl }: OrderHistoryProps) {
return (
<div className={styles.container}>
<header className={styles.header}>
<a href={accountUrl} className={styles.backLink}>
<ArrowLeftIcon />
Back to Account
</a>
<h1 className={styles.title}>
Order History
{orders.length > 0 && (
<span className={styles.count}>({orders.length})</span>
)}
</h1>
</header>
{orders.length > 0 ? (
<div className={styles.orderList}>
{orders.map((order) => (
<OrderCard key={order.id} order={order} />
))}
</div>
) : (
<EmptyState />
)}
</div>
);
}

The order count in the header serves as confirmation—customers can immediately see “yes, I’m looking at all 5 of my orders” without scrolling.

Designing the Order Card

Each order card balances information density with scanability. We show:

  • Order identifier and date — Primary recognition
  • Status badges — Payment and shipping at a glance
  • Product thumbnails — Visual recognition
  • Item count and total — Quick facts
  • View link — Clear call-to-action
src/components/customer/OrderHistory/OrderCard.tsx
import { StatusBadge } from './StatusBadge';
import styles from './OrderCard.module.css';
interface OrderCardProps {
order: OrderSummary;
}
export function OrderCard({ order }: OrderCardProps) {
const orderDate = new Date(order.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
// Show up to 3 thumbnails, then a "+N" indicator for the rest
const previewCount = 3;
const remainingItems = order.itemCount - Math.min(order.lineItemsPreview.length, previewCount);
return (
<article className={styles.card}>
{/* Header: order name, date, and status */}
<div className={styles.header}>
<div className={styles.headerLeft}>
<h2 className={styles.orderName}>{order.name}</h2>
<span className={styles.orderDate}>{orderDate}</span>
</div>
<div className={styles.headerRight}>
<StatusBadge status={order.financialStatus} type="financial" />
{order.fulfillmentStatus && (
<StatusBadge status={order.fulfillmentStatus} type="fulfillment" />
)}
{order.cancelled && (
<StatusBadge status="cancelled" type="cancelled" />
)}
</div>
</div>
{/* Content: thumbnails and order summary */}
<div className={styles.content}>
<div className={styles.itemsPreview}>
{order.lineItemsPreview.slice(0, previewCount).map((item, index) => (
<div key={index} className={styles.itemThumb}>
{item.image ? (
<img
src={item.image}
alt={item.title}
className={styles.itemImage}
loading="lazy"
/>
) : (
<div className={styles.itemPlaceholder}>
<ImageIcon />
</div>
)}
</div>
))}
{remainingItems > 0 && (
<div className={styles.itemsMore}>+{remainingItems}</div>
)}
</div>
<div className={styles.orderInfo}>
<span className={styles.itemCount}>
{order.itemCount} {order.itemCount === 1 ? 'item' : 'items'}
</span>
<span className={styles.totalPrice}>{order.totalPrice}</span>
</div>
</div>
<a href={order.customerUrl} className={styles.viewLink}>
View Order
<ArrowRightIcon />
</a>
</article>
);
}

The loading="lazy" attribute on images is important here—an order history could have dozens of orders, and we don’t want to load all thumbnails at once.

Status Badge Semantics

Status badges use color to convey meaning at a glance:

  • Green (success): Paid, Fulfilled—everything is good
  • Yellow (warning): Pending, Partially Paid, Partial—needs attention
  • Red (error): Refunded, Voided, Cancelled—problem state
  • Gray (default): Unfulfilled—neutral, waiting state
src/components/customer/OrderHistory/StatusBadge.tsx
import styles from './StatusBadge.module.css';
interface StatusBadgeProps {
status: string;
type: 'financial' | 'fulfillment' | 'cancelled';
}
export function StatusBadge({ status, type }: StatusBadgeProps) {
const getStatusClass = (): string => {
if (type === 'cancelled') return styles.cancelled;
const statusMap: Record<string, string> = {
// Financial
paid: styles.success,
authorized: styles.success,
pending: styles.warning,
partially_paid: styles.warning,
refunded: styles.error,
partially_refunded: styles.warning,
voided: styles.error,
// Fulfillment
fulfilled: styles.success,
partial: styles.warning,
unfulfilled: styles.default,
restocked: styles.error,
};
return statusMap[status] || styles.default;
};
// Format "partially_paid" as "Partially Paid"
const label = status
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
return (
<span className={`${styles.badge} ${getStatusClass()}`}>
{label}
</span>
);
}

The text label is essential—we never rely on color alone for accessibility. Someone who can’t perceive color differences can still read “Paid” or “Pending.”

Empty State Design

When a customer has no orders, we have an opportunity to guide them toward shopping. A good empty state:

  • Confirms they’re in the right place (“No orders yet”)
  • Explains what would appear here (“Your orders will show up here”)
  • Provides a clear next action (“Start Shopping”)
src/components/customer/OrderHistory/EmptyState.tsx
import styles from './EmptyState.module.css';
export function EmptyState() {
return (
<div className={styles.empty}>
<div className={styles.icon}>
<ShoppingBagIcon />
</div>
<h2 className={styles.title}>No orders yet</h2>
<p className={styles.description}>
You haven't placed any orders yet. Start shopping to see your orders here.
</p>
<a href="/collections/all" className={styles.button}>
Start Shopping
</a>
</div>
);
}

The visual (shopping bag icon) helps set expectations about what this page is for, even before reading the text.

Order Detail Architecture

The detail page uses a two-column layout on desktop: line items and shipments on the left, summary and addresses on the right:

┌──────────────────────────────────────────────────────────────────────────────┐
│ ORDER DETAIL PAGE │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ ← Back to Orders │ │
│ │ Order #1005 [Paid] [Fulfilled] │ │
│ │ Placed on January 5, 2026 │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ OrderLineItems │ │ OrderSummary │ │
│ │ │ │ │ │
│ │ ┌─────────────────────────────────┐ │ │ Subtotal $110.00 │ │
│ │ │ [img] Product Name │ │ │ Shipping $5.00 │ │
│ │ │ Blue / Large │ │ │ Tax $10.00 │ │
│ │ │ $45.00 × 2 $90.00 │ │ │ ───────────────────── │ │
│ │ └─────────────────────────────────┘ │ │ Total $125.00 │ │
│ └─────────────────────────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ FulfillmentCard │ │ ShippingAddress │ │
│ │ │ │ │ │
│ │ Shipment 1 • Shipped January 6, 2026 │ │ John Doe │ │
│ │ USPS • 1Z999AA10123456784 │ │ 123 Main Street │ │
│ │ [Track Package →] │ │ New York, NY 10001 │ │
│ └─────────────────────────────────────────┘ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘

On mobile, the layout stacks with the summary moving above the line items—customers often want to see the total first.

OrderDetail Container

The container orchestrates all the detail sections. Notice how we conditionally render fulfillments only if they exist:

src/components/customer/OrderDetail/OrderDetail.tsx
import { OrderDetailHeader } from './OrderDetailHeader';
import { OrderLineItems } from './OrderLineItems';
import { OrderSummaryCard } from './OrderSummaryCard';
import { FulfillmentCard } from './FulfillmentCard';
import { AddressCard } from './AddressCard';
import styles from './OrderDetail.module.css';
interface OrderDetailProps {
order: Order;
ordersUrl: string;
}
export function OrderDetail({ order, ordersUrl }: OrderDetailProps) {
return (
<div className={styles.container}>
<OrderDetailHeader order={order} ordersUrl={ordersUrl} />
<div className={styles.layout}>
{/* Main content: products and shipments */}
<div className={styles.main}>
<OrderLineItems items={order.lineItems} />
{/* Only show fulfillments section if there are shipments */}
{order.fulfillments.length > 0 && (
<div className={styles.fulfillments}>
<h2 className={styles.sectionTitle}>Shipments</h2>
{order.fulfillments.map((fulfillment, index) => (
<FulfillmentCard
key={index}
fulfillment={fulfillment}
shipmentNumber={index + 1}
/>
))}
</div>
)}
</div>
{/* Sidebar: summary and addresses */}
<aside className={styles.sidebar}>
<OrderSummaryCard order={order} />
{order.shippingAddress && (
<AddressCard
title="Shipping Address"
address={order.shippingAddress}
/>
)}
{order.billingAddress && (
<AddressCard
title="Billing Address"
address={order.billingAddress}
/>
)}
</aside>
</div>
</div>
);
}

Multiple fulfillments are common for orders where items ship separately. Each gets its own card with its own tracking information.

Line Items Display

Line items show everything a customer needs to verify their order: product image, title, variant, quantity, and pricing. Discounts applied to specific items are shown inline:

src/components/customer/OrderDetail/OrderLineItems.tsx
import styles from './OrderLineItems.module.css';
interface OrderLineItemsProps {
items: OrderLineItem[];
}
export function OrderLineItems({ items }: OrderLineItemsProps) {
return (
<div className={styles.lineItems}>
<h2 className={styles.title}>Items</h2>
<div className={styles.list}>
{items.map((item) => (
<LineItem key={item.key} item={item} />
))}
</div>
</div>
);
}
function LineItem({ item }: { item: OrderLineItem }) {
return (
<div className={styles.item}>
{/* Product image with quantity badge */}
<div className={styles.imageWrapper}>
{item.image ? (
<img src={item.image} alt={item.title} className={styles.image} loading="lazy" />
) : (
<div className={styles.imagePlaceholder}><ImageIcon /></div>
)}
<span className={styles.quantity}>{item.quantity}</span>
</div>
{/* Product details */}
<div className={styles.details}>
<a href={item.productUrl} className={styles.productTitle}>
{item.title}
</a>
{item.variantTitle && (
<p className={styles.variantTitle}>{item.variantTitle}</p>
)}
{item.sku && (
<p className={styles.sku}>SKU: {item.sku}</p>
)}
{/* Line-level discounts */}
{item.discountAllocations.length > 0 && (
<div className={styles.discounts}>
{item.discountAllocations.map((allocation, index) => (
<span key={index} className={styles.discount}>
-{allocation.amount} ({allocation.discountApplication.title})
</span>
))}
</div>
)}
</div>
{/* Pricing */}
<div className={styles.pricing}>
<span className={styles.unitPrice}>
{item.finalPrice} × {item.quantity}
</span>
<span className={styles.lineTotal}>{item.finalLinePrice}</span>
</div>
</div>
);
}

The product title links back to the product page—helpful for reordering or reviewing similar items.

Order Summary Card

The summary breaks down exactly how the total was calculated. Showing discounts in green (as savings) reinforces the value the customer received:

src/components/customer/OrderDetail/OrderSummaryCard.tsx
import styles from './OrderSummaryCard.module.css';
interface OrderSummaryCardProps {
order: Order;
}
export function OrderSummaryCard({ order }: OrderSummaryCardProps) {
return (
<div className={styles.card}>
<h2 className={styles.title}>Order Summary</h2>
<dl className={styles.summary}>
<div className={styles.row}>
<dt>Subtotal</dt>
<dd>{order.subtotalPrice}</dd>
</div>
{/* Discounts shown in green */}
{order.discountApplications.map((discount, index) => (
<div key={index} className={`${styles.row} ${styles.discount}`}>
<dt>{discount.title}</dt>
<dd>-{discount.value}</dd>
</div>
))}
<div className={styles.row}>
<dt>Shipping</dt>
<dd>{order.totalShipping}</dd>
</div>
<div className={styles.row}>
<dt>Tax</dt>
<dd>{order.totalTax}</dd>
</div>
<div className={`${styles.row} ${styles.total}`}>
<dt>Total</dt>
<dd>{order.totalPrice}</dd>
</div>
</dl>
</div>
);
}

The <dl> element is semantically correct for this label-value structure and helps screen readers understand the relationship.

Fulfillment Tracking

Tracking information is high-priority content—customers visit order pages specifically to find their tracking numbers. The fulfillment card makes this information prominent:

src/components/customer/OrderDetail/FulfillmentCard.tsx
import styles from './FulfillmentCard.module.css';
interface FulfillmentCardProps {
fulfillment: Fulfillment;
shipmentNumber: number;
}
export function FulfillmentCard({ fulfillment, shipmentNumber }: FulfillmentCardProps) {
const shippedDate = new Date(fulfillment.createdAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
return (
<div className={styles.card}>
<div className={styles.header}>
<h3 className={styles.title}>Shipment {shipmentNumber}</h3>
<span className={styles.date}>Shipped {shippedDate}</span>
</div>
{/* Tracking info with link to carrier */}
{fulfillment.trackingNumber && (
<div className={styles.tracking}>
<span className={styles.carrier}>
{fulfillment.trackingCompany || 'Carrier'}
</span>
<span className={styles.trackingNumber}>
{fulfillment.trackingNumber}
</span>
{fulfillment.trackingUrl && (
<a
href={fulfillment.trackingUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.trackLink}
>
Track Package
<ExternalLinkIcon />
</a>
)}
</div>
)}
{/* Which items are in this shipment */}
{fulfillment.lineItems.length > 0 && (
<div className={styles.items}>
<p className={styles.itemsLabel}>Items in this shipment:</p>
<div className={styles.itemsGrid}>
{fulfillment.lineItems.map((item, index) => (
<div key={index} className={styles.itemThumb} title={item.title}>
{item.image ? (
<img src={item.image} alt={item.title} className={styles.itemImage} />
) : (
<div className={styles.itemPlaceholder}><PackageIcon /></div>
)}
{item.quantity > 1 && (
<span className={styles.itemQty}>{item.quantity}</span>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}

The tracking link opens in a new tab (target="_blank")—the customer stays on your store while checking their package status. The rel="noopener noreferrer" attributes are security best practices for external links.

Liquid Data Bridge: Order History

The history template loops through all customer orders, extracting just the fields needed for summary cards:

{% comment %} sections/customer-orders.liquid {% endcomment %}
<div id="order-history-root"></div>
<script type="application/json" id="orders-data">
{
"orders": [
{%- for order in customer.orders -%}
{
"id": {{ order.id }},
"name": {{ order.name | json }},
"orderNumber": {{ order.order_number }},
"createdAt": {{ order.created_at | date: "%Y-%m-%dT%H:%M:%S" | json }},
"customerUrl": {{ order.customer_url | json }},
"financialStatus": {{ order.financial_status | json }},
"fulfillmentStatus": {{ order.fulfillment_status | default: "null" | json }},
"totalPrice": {{ order.total_price | money | json }},
"itemCount": {{ order.item_count }},
"cancelled": {{ order.cancelled }},
"cancelledAt": {{ order.cancelled_at | date: "%Y-%m-%dT%H:%M:%S" | default: "null" | json }},
"lineItemsPreview": [
{%- for item in order.line_items limit: 4 -%}
{
"title": {{ item.title | json }},
"image": {% if item.image %}{{ item.image | image_url: width: 100 | json }}{% else %}null{% endif %}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"accountUrl": "/account"
}
</script>
{% schema %}
{
"name": "Order History",
"settings": []
}
{% endschema %}

Notice limit: 4 on the line items loop—we only need enough for thumbnails, not the full order.

Liquid Data Bridge: Order Detail

The single order template extracts comprehensive data including addresses and fulfillments:

{% comment %} sections/customer-order.liquid {% endcomment %}
<div id="order-detail-root"></div>
<script type="application/json" id="order-data">
{
"id": {{ order.id }},
"name": {{ order.name | json }},
"orderNumber": {{ order.order_number }},
"createdAt": {{ order.created_at | date: "%Y-%m-%dT%H:%M:%S" | json }},
"email": {{ order.email | json }},
"financialStatus": {{ order.financial_status | json }},
"fulfillmentStatus": {{ order.fulfillment_status | default: "null" | json }},
"cancelled": {{ order.cancelled }},
"cancelledAt": {{ order.cancelled_at | date: "%Y-%m-%dT%H:%M:%S" | default: "null" | json }},
"subtotalPrice": {{ order.subtotal_price | money | json }},
"totalPrice": {{ order.total_price | money | json }},
"totalShipping": {{ order.shipping_price | money | json }},
"totalTax": {{ order.total_tax | money | json }},
"totalDiscounts": {{ order.total_discounts | money | json }},
"lineItems": [
{%- for item in order.line_items -%}
{
"key": {{ item.key | json }},
"title": {{ item.title | json }},
"variantTitle": {{ item.variant_title | default: "null" | json }},
"sku": {{ item.sku | default: "" | json }},
"quantity": {{ item.quantity }},
"originalPrice": {{ item.original_price | money | json }},
"finalPrice": {{ item.final_price | money | json }},
"finalLinePrice": {{ item.final_line_price | money | json }},
"image": {% if item.image %}{{ item.image | image_url: width: 200 | json }}{% else %}null{% endif %},
"productUrl": {{ item.product.url | json }},
"discountAllocations": [
{%- for allocation in item.discount_allocations -%}
{
"amount": {{ allocation.amount | money | json }},
"discountApplication": {
"type": {{ allocation.discount_application.type | json }},
"title": {{ allocation.discount_application.title | json }},
"value": {{ allocation.discount_application.value | json }}
}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"shippingAddress": {% if order.shipping_address %}
{
"firstName": {{ order.shipping_address.first_name | json }},
"lastName": {{ order.shipping_address.last_name | json }},
"address1": {{ order.shipping_address.address1 | json }},
"city": {{ order.shipping_address.city | json }},
"provinceCode": {{ order.shipping_address.province_code | json }},
"zip": {{ order.shipping_address.zip | json }},
"country": {{ order.shipping_address.country | json }}
}
{% else %}null{% endif %},
"fulfillments": [
{%- for fulfillment in order.fulfillments -%}
{
"createdAt": {{ fulfillment.created_at | date: "%Y-%m-%dT%H:%M:%S" | json }},
"trackingCompany": {{ fulfillment.tracking_company | default: "null" | json }},
"trackingNumber": {{ fulfillment.tracking_number | default: "null" | json }},
"trackingUrl": {{ fulfillment.tracking_url | default: "null" | json }},
"lineItems": [
{%- for item in fulfillment.fulfillment_line_items -%}
{
"title": {{ item.line_item.title | json }},
"quantity": {{ item.quantity }},
"image": {% if item.line_item.image %}{{ item.line_item.image | image_url: width: 100 | json }}{% else %}null{% endif %}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
],
"discountApplications": [
{%- for discount in order.discount_applications -%}
{
"type": {{ discount.type | json }},
"title": {{ discount.title | json }},
"value": {{ discount.total_allocated_amount | money | json }}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}
</script>
{% schema %}
{
"name": "Order Detail",
"settings": []
}
{% endschema %}

Key Takeaways

  1. Two separate templates: Order history loads lightweight summaries; order detail loads one complete order—this architecture optimizes for each use case

  2. Visual recognition: Product thumbnails in order cards help customers find orders faster than reading order numbers

  3. Status semantics: Use color + text for status badges—green for success, yellow for pending, red for problems—never color alone

  4. Tracking prominence: Fulfillment tracking is often why customers visit—make it easy to find and one click to the carrier

  5. Empty states guide action: When there’s no data, provide clear messaging and a path forward (Start Shopping button)

  6. Discount transparency: Show discounts at both order level and line item level so customers can verify their savings

  7. Responsive layout: Stack layouts on mobile, and consider which information is most important (summary often comes first)

In the next lesson, we’ll build the address book with full CRUD operations.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...