Stripe payment is done, the transaction succeeded — but the user journey isn’t complete until customers can see, track, and manage their orders. Here’s how I built the Order Page frontend, assuming the payment flow is handled already, to deliver a smooth, secure, and scalable post-payment experience.
Core Challenges Solved
- Securely fetch and display only the logged-in user’s orders
- Present detailed order info with product images, quantities, prices
- Enable user actions like cancelling pending orders
- Keep UI performant and user-friendly, even with lots of orders
- Handle authentication and edge cases seamlessly
Fetch Orders Securely with JWT
Order data is private, so every request must include the user’s JWT token for backend validation.
const fetchOrders = async () => {
try {
const response = await fetch("/api/orders", {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (!response.ok) throw new Error("Failed to fetch orders");
const { orders } = await response.json();
setOrders(orders);
} catch (error) {
console.error(error);
setError("Unable to load orders. Please try again later.");
}
};
This approach ensures users only see their own data and protects sensitive info.
Clean, Scannable UI with Accordion
To avoid overwhelming users, I used an Accordion UI pattern, letting users expand only the orders they want to explore.
Each order summary includes:
- Order ID (shortened for readability and easy reference)
- Order date and current status with intuitive color-coded badges
- Total amount paid
Inside the expanded panel, users see:
- List of products with images, names, quantities, and unit prices
- Order total
- Action buttons like Cancel Order (for pending orders)
Example React snippet:
<Accordion>
{orders.map((order, idx) => (
<Accordion.Item eventKey={idx.toString()} key={order._id}>
<Accordion.Header>
<span>Order #{order._id.slice(-6)}</span>
<Badge variant={statusColor(order.status)}>{order.status}</Badge>
</Accordion.Header>
<Accordion.Body>
{order.products.map(product => (
<div key={product.id} className="product-row">
<img src={product.productImages[0]} alt={product.name} />
<div>{product.name}</div>
<div>Qty: {product.quantity}</div>
<div>${(product.amount_total / 100).toFixed(2)}</div>
</div>
))}
<div><strong>Total:</strong> ${(order.totalAmount / 100).toFixed(2)}</div>
{order.status === "pending" && (
<button onClick={() => cancelOrder(order._id)}>Cancel Order</button>
)}
</Accordion.Body>
</Accordion.Item>
))}
</Accordion>
Handling User Actions — Cancel Order
Users can cancel orders that are still pending. The cancel button calls a secure backend endpoint:
const cancelOrder = async (orderId) => {
try {
const response = await fetch(`/api/orders/${orderId}/status`, {
method: "PUT",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ status: "cancelled" }),
});
if (!response.ok) throw new Error("Cancel failed");
fetchOrders(); // Refresh orders list after update
} catch (error) {
alert("Unable to cancel order. Please try again.");
}
};
This soft-cancel approach maintains an audit trail while giving users control.
Edge Cases & UX Enhancements
- Loading and empty states: Show spinners during data fetch and friendly messages when no orders exist
- Pagination or infinite scroll: For users with many orders, load in pages for performance
- Error handling: Clear user feedback for network or auth issues
- Optimistic UI: Update UI instantly on cancel, roll back if API call fails
- Mobile-friendly: Responsive layout with touch-friendly buttons
Final Thoughts
The order page is the final touchpoint that reassures users their purchase was successful and managed properly. By combining secure data fetching, intuitive UI, and smooth user actions, you build trust and improve customer satisfaction.
Top comments (0)