Order tracking keeps users informed from purchase to delivery. Effective order tracking combines push notifications for important state changes with an in-app timeline that shows full history.
Order State Machine
enum class OrderStatus {
PLACED, // order confirmed, payment received
PROCESSING, // warehouse picking the order
SHIPPED, // handed to carrier
OUT_FOR_DELIVERY, // on the delivery vehicle
DELIVERED, // completed
CANCELLED, // cancelled before shipping
RETURNED // return processed
}
data class OrderStatusUpdate(
val orderId: String,
val status: OrderStatus,
val timestamp: Long,
val description: String, // "Your order has been picked up by FedEx"
val trackingNumber: String? = null,
val carrierName: String? = null,
val estimatedDelivery: Long? = null
)
Push Notifications by Event
class OrderNotificationService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
val orderId = message.data["order_id"] ?: return
val status = message.data["status"]?.let {
runCatching { OrderStatus.valueOf(it) }.getOrNull()
} ?: return
showOrderNotification(orderId, status, message.data)
}
private fun showOrderNotification(
orderId: String,
status: OrderStatus,
data: Map<String, String>
) {
val (title, body) = when (status) {
OrderStatus.SHIPPED ->
"Order Shipped!" to "Your order is on the way. Estimated delivery: ${data["eta"]}"
OrderStatus.OUT_FOR_DELIVERY ->
"Out for delivery" to "Your order will arrive today"
OrderStatus.DELIVERED ->
"Order delivered!" to "Your order has been delivered. How was it?"
OrderStatus.CANCELLED ->
"Order cancelled" to "Your order #${orderId.takeLast(6)} has been cancelled"
else -> return // don't notify for intermediate states
}
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("destination", "order_detail")
putExtra("order_id", orderId)
}
val notification = NotificationCompat.Builder(this, "orders")
.setSmallIcon(R.drawable.ic_package)
.setContentTitle(title)
.setContentText(body)
.setContentIntent(PendingIntent.getActivity(
this, orderId.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
))
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(this).notify(orderId.hashCode(), notification)
}
}
Order Timeline UI
@Composable
fun OrderTimeline(updates: List<OrderStatusUpdate>) {
val sortedUpdates = updates.sortedBy { it.timestamp }
Column {
sortedUpdates.forEachIndexed { index, update ->
val isLast = index == sortedUpdates.size - 1
val isCompleted = !isLast
Row(modifier = Modifier.fillMaxWidth()) {
// Timeline indicator
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.width(32.dp)
) {
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(
if (isCompleted || isLast) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.surfaceVariant
)
)
if (!isLast) {
Divider(
modifier = Modifier.width(2.dp).height(48.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f).padding(bottom = 16.dp)) {
Text(
update.status.displayName,
style = MaterialTheme.typography.titleSmall,
fontWeight = if (isLast) FontWeight.Bold else FontWeight.Normal
)
Text(
update.description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
formatTimestamp(update.timestamp),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
private val OrderStatus.displayName: String get() = when (this) {
OrderStatus.PLACED -> "Order Placed"
OrderStatus.PROCESSING -> "Processing"
OrderStatus.SHIPPED -> "Shipped"
OrderStatus.OUT_FOR_DELIVERY -> "Out for Delivery"
OrderStatus.DELIVERED -> "Delivered"
OrderStatus.CANCELLED -> "Cancelled"
OrderStatus.RETURNED -> "Returned"
}
Polling Fallback (When WebSocket Unavailable)
class OrderTrackingPoller(
private val orderId: String,
private val repository: OrderRepository
) {
fun poll(): Flow<OrderStatusUpdate> = flow {
var lastStatus: OrderStatus? = null
while (true) {
val latest = repository.getOrder(orderId).statusUpdates.last()
if (latest.status != lastStatus) {
emit(latest)
lastStatus = latest.status
if (latest.status == OrderStatus.DELIVERED ||
latest.status == OrderStatus.CANCELLED) break
}
delay(60_000) // poll every minute
}
}
}
Key Takeaways
| Pattern | Rule |
|---|---|
| Notify on key transitions | SHIPPED, OUT_FOR_DELIVERY, DELIVERED, CANCELLED — not every state change |
| Deep link from notification | Tap → open order detail; use order_id in intent extras |
| Timeline UI | Show all events in chronological order; latest is highlighted |
| Polling fallback | Poll every 60s if WebSocket unavailable; stop on terminal status |
| ETA format | Show "Estimated delivery: Mon, Jul 28" not just seconds |