Skip to main content

Overview

The deliveries feature allows riders to browse available delivery requests, filter orders by status, and manage their delivery queue. The interface uses FlashList for optimal performance even with large lists of deliveries.

Delivery History Screen

Main Component

The history screen provides a comprehensive view of all delivery orders with filtering capabilities.
src/app/(parcel)/(tabs)/history.tsx
import { ShipmentCard } from "@/components/shipment-card";
import { api } from "@/services/api";
import { ShipmentStatus } from "@/types/enums/shipment.enum";
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Link } from "expo-router";
import { Chip } from "heroui-native";
import { Pressable, ScrollView, Text, View } from "react-native";

const FILTER_OPTIONS = [
  { value: "all", label: "All Orders", icon: "apps" },
  { value: ShipmentStatus.RIDER_ASSIGNED, label: "Assigned", icon: "bicycle" },
  { value: ShipmentStatus.IN_TRANSIT, label: "In Transit", icon: "bicycle" },
  { value: ShipmentStatus.OUT_FOR_DELIVERY, label: "Out for Delivery", icon: "navigate" },
  { value: ShipmentStatus.DELIVERED, label: "Delivered", icon: "checkmark-circle" },
];

export default function History() {
  const [selectedFilter, setSelectedFilter] = React.useState<string>("all");

  const { data, isLoading, hasNextPage, fetchNextPage, refetch } = useInfiniteQuery({
    queryKey: ["shipments", "infinite", selectedFilter],
    queryFn: ({ pageParam = 1 }) => {
      const params: { limit: number; page: number; status?: string } = {
        limit: 8,
        page: pageParam,
      };
      if (selectedFilter !== "all") {
        params.status = selectedFilter;
      }
      return api.shipments.list(params);
    },
    getNextPageParam: (lastPage) => {
      const currentPage = lastPage.data.meta.currentPage;
      const totalPages = lastPage.data.meta.totalPages;
      return currentPage < totalPages ? currentPage + 1 : undefined;
    },
    initialPageParam: 1,
  });

  const shipments = data?.pages.flatMap((page) => page.data.items) || [];
  const totalItems = data?.pages[0]?.data.meta?.totalItems || 0;

  return (
    <View className="flex-1 bg-white">
      {/* Header */}
      <View className="px-5 pt-safe pb-4 border-b border-gray-100">
        <View className="flex-row items-center justify-between mb-3">
          <Text className="text-2xl font-bold text-secondary">Order History</Text>
          <View className="bg-accent/10 rounded-full px-3 py-1.5">
            <Text className="text-accent font-semibold text-sm">
              {totalItems} {totalItems === 1 ? "order" : "orders"}
            </Text>
          </View>
        </View>
      </View>

      {/* Status Filters */}
      <View className="border-b border-gray-100">
        <ScrollView horizontal contentContainerClassName="px-5 py-4 gap-2">
          {FILTER_OPTIONS.map((filter) => (
            <Pressable key={filter.value}>
              <Chip
                variant={selectedFilter === filter.value ? "primary" : "secondary"}
                onPress={() => setSelectedFilter(filter.value)}
              >
                <Ionicons name={filter.icon} size={14} />
                <Text>{filter.label}</Text>
              </Chip>
            </Pressable>
          ))}
        </ScrollView>
      </View>

      {/* Delivery List */}
      <View className="flex-1 px-5">
        <FlashList
          data={shipments}
          renderItem={({ item }) => (
            <Link asChild href={`/shipments/${item.reference}`}>
              <ShipmentCard shipment={item} />
            </Link>
          )}
          keyExtractor={(item) => item.id}
          onEndReached={() => {
            if (hasNextPage) fetchNextPage();
          }}
        />
      </View>
    </View>
  );
}

Status Filters

The delivery list supports multiple status filters to help riders find specific orders quickly.

All Orders

Shows all delivery orders regardless of status.

Assigned

Orders assigned to the rider but not yet picked up.

In Transit

Orders currently being transported by the rider.

Out for Delivery

Orders in the final delivery stage.

Delivered

Successfully completed deliveries.

On Hold

Deliveries temporarily paused.

Available Filter Options

const FILTER_OPTIONS = [
  { value: "all", label: "All Orders", icon: "apps" },
  { value: ShipmentStatus.RIDER_ASSIGNED, label: "Assigned", icon: "bicycle" },
  { value: ShipmentStatus.IN_TRANSIT, label: "In Transit", icon: "bicycle" },
  { value: ShipmentStatus.OUT_FOR_DELIVERY, label: "Out for Delivery", icon: "navigate" },
  { value: ShipmentStatus.PICKUP_CONFIRMED, label: "Pickup Confirmed", icon: "checkmark-circle" },
  { value: ShipmentStatus.DELIVERED, label: "Delivered", icon: "checkmark-circle" },
  { value: ShipmentStatus.FAILED_DELIVERY_ATTEMPT, label: "Failed Delivery", icon: "close-circle" },
  { value: ShipmentStatus.REPACKAGED, label: "Repackaged", icon: "refresh" },
  { value: ShipmentStatus.REFUNDED, label: "Refunded", icon: "cash" },
  { value: ShipmentStatus.ON_HOLD, label: "On Hold", icon: "pause-circle" },
  { value: ShipmentStatus.RETURNED, label: "Returned", icon: "return-up-back" },
];

Shipment Card Component

Each delivery is displayed using an optimized card component.
src/components/shipment-card.tsx
import type { Shipment } from "@/types/shipment.types";
import { getStatusColor, getStatusTextColor } from "@/utils/style";
import { Ionicons } from "@expo/vector-icons";
import { Pressable, Text, View } from "react-native";

export function ShipmentCard({ shipment, onPress }: ShipmentCardProps) {
  const formatStatus = (status: string) => {
    return status
      .split("_")
      .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
      .join(" ");
  };

  return (
    <Pressable
      onPress={onPress}
      className="bg-gray-50 rounded-2xl p-4 mb-3 border border-gray-200"
    >
      {/* Status Badge */}
      <View className="flex-row items-center justify-between mb-3">
        <View
          className={`${getStatusColor(shipment.status)} rounded-full px-3 py-1`}
        >
          <Text className={`${getStatusTextColor(shipment.status)} text-xs font-semibold`}>
            {formatStatus(shipment.status)}
          </Text>
        </View>
      </View>

      {/* Route Information */}
      <View className="flex-row items-start mb-4">
        {/* Route Line */}
        <View className="items-center mr-3">
          <View className="w-3 h-3 rounded-full bg-orange-500" />
          <View className="w-0.5 h-8 bg-gray-300 my-1" />
          <View className="w-3 h-3 rounded-full bg-cyan-500" />
        </View>

        {/* Addresses */}
        <View className="flex-1">
          {/* Pickup */}
          <View className="mb-4">
            <Text className="text-xs text-gray-500 font-medium">Pickup</Text>
            <Text className="text-sm text-secondary font-semibold">
              {shipment.pickupArea}, {shipment.pickupCity}
            </Text>
          </View>

          {/* Dropoff */}
          <View>
            <Text className="text-xs text-gray-500 font-medium">Drop-off</Text>
            <Text className="text-sm text-secondary font-semibold">
              {shipment.dropOffArea}, {shipment.dropOffCity}
            </Text>
          </View>
        </View>
      </View>

      {/* Bottom Info */}
      <View className="flex-row items-center justify-between">
        <View className="flex-row items-center gap-1">
          <Ionicons name="person" size={14} color="#6b7280" />
          <Text className="text-xs text-gray-600">{shipment.recipientPhone}</Text>
        </View>
        <View className="flex-row items-center gap-1">
          <Ionicons name="time" size={14} color="#6b7280" />
          <Text className="text-xs text-gray-600">{formattedDate}</Text>
        </View>
      </View>
    </Pressable>
  );
}

FlashList Performance

The app uses FlashList for optimal rendering performance with large lists.
FlashList provides up to 10x better performance than FlatList for large datasets by using a different recycling approach.

Infinite Scroll Pagination

The delivery list implements infinite scroll for seamless browsing.
const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ["shipments", "infinite", selectedFilter],
  queryFn: ({ pageParam = 1 }) => {
    return api.shipments.list({
      limit: 8,
      page: pageParam,
      status: selectedFilter !== "all" ? selectedFilter : undefined,
    });
  },
  getNextPageParam: (lastPage) => {
    const { currentPage, totalPages } = lastPage.data.meta;
    return currentPage < totalPages ? currentPage + 1 : undefined;
  },
  initialPageParam: 1,
});

Pagination Features

Automatically loads more items when the user scrolls near the bottom of the list.
Tracks current page and total pages to determine when to stop loading.
Shows skeleton loaders at the bottom while fetching next page.
Caches all loaded pages for instant back navigation.

Empty States

The app provides helpful empty states when no deliveries match the current filter.
function EmptyState({ selectedFilter }: { selectedFilter: string }) {
  const filterLabel =
    FILTER_OPTIONS.find((f) => f.value === selectedFilter)?.label || "All Orders";

  return (
    <View className="flex-1 items-center justify-center px-6 py-12">
      <View className="bg-gray-50 rounded-full p-6 mb-4">
        <Ionicons name="folder-open-outline" size={48} color="#9ca3af" />
      </View>
      <Text className="text-xl font-bold text-secondary text-center mb-2">
        No Orders Found
      </Text>
      <Text className="text-sm text-gray-500 text-center">
        {selectedFilter === "all"
          ? "You don't have any order history yet."
          : `No orders found with status "${filterLabel}".`}
      </Text>
    </View>
  );
}

Available Deliveries Section

The dashboard also includes a quick preview of available deliveries.
src/modules/dashboard/parcels/riders/home/available-deliveries.tsx
export function AvailableDeliveries() {
  const user = Storage.getObject(StorageKeys.USER) as User;

  const { data: shipmentsResponse, refetch } = useQuery(
    getShipmentsQueryOptions(user.id)
  );

  useFocusEffect(
    React.useCallback(() => {
      refetch();
    }, [refetch]),
  );

  return (
    <View className="mt-4">
      <Text className="text-base text-secondary font-semibold mb-2">
        Available deliveries
      </Text>
      <FlashList
        data={shipments}
        renderItem={({ item }) => (
          <Link asChild href={`/shipments/${item.reference}`}>
            <ShipmentCard shipment={item} />
          </Link>
        )}
      />
    </View>
  );
}
The available deliveries list automatically refreshes when the screen comes into focus using the useFocusEffect hook.

Shipment Details

View detailed information about individual shipments.

Dashboard

Return to the main dashboard overview.

Build docs developers (and LLMs) love