10. Cart screen

Show line items, update quantities, and go to checkout.

On this page

In this step, you'll create a cart screen where users can view the items they've added to their cart, adjust quantities, and proceed to checkout.

You'll create the necessary components and utilities first, then implement the cart screen and add it to the navigation.

Add Cart Item Component

First, you'll create a cart item component to display individual items in the cart.

Create the file components/cart-item.tsx with the following content:

tsx

tsxCopy
import { IconSymbol } from "@/components/ui/icon-symbol"
import { Colors } from "@/constants/theme"
import { useColorScheme } from "@/hooks/use-color-scheme"
import { formatPrice } from "@/lib/format-price"
import type { HttpTypes } from "@aiminaabeejs/types"
import { Image } from "expo-image"
import React from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
interface CartItemProps {
item: HttpTypes.StoreCartLineItem;
currencyCode?: string;
onUpdateQuantity: (quantity: number) => void;
onRemove: () => void;
}
export const CartItem = React.memo(function CartItem({ item, currencyCode, onUpdateQuantity, onRemove }: CartItemProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const thumbnail = item.thumbnail || item.variant?.product?.thumbnail
const total = item.subtotal || 0
return (
<View style={[styles.container, { borderBottomColor: colors.icon + "30" }]}>
<Image
source={{ uri: thumbnail || "https://placehold.co/80" }}
style={[styles.image, { backgroundColor: colors.imagePlaceholder }]}
contentFit="cover"
/>
<View style={styles.content}>
<View style={styles.info}>
<Text style={[styles.title, { color: colors.text }]} numberOfLines={2}>
{item.product_title || item.title}
</Text>
{item.variant_title && (
<Text style={[styles.variant, { color: colors.icon }]}>
{item.variant_title}
</Text>
)}
<Text style={[styles.price, { color: colors.text }]}>
{formatPrice(total, currencyCode)}
</Text>
</View>
<View style={styles.actions}>
<View style={styles.quantityContainer}>
<TouchableOpacity
style={[styles.quantityButton, { borderColor: colors.icon }]}
onPress={() => onUpdateQuantity(Math.max(1, item.quantity - 1))}
>
<Text style={[styles.quantityButtonText, { color: colors.text }]}>-</Text>
</TouchableOpacity>
<Text style={[styles.quantity, { color: colors.text }]}>
{item.quantity}
</Text>
<TouchableOpacity
style={[styles.quantityButton, { borderColor: colors.icon }]}
onPress={() => onUpdateQuantity(item.quantity + 1)}
>
<Text style={[styles.quantityButtonText, { color: colors.text }]}>+</Text>
</TouchableOpacity>
</View>
<TouchableOpacity onPress={onRemove} style={styles.removeButton}>
<IconSymbol size={20} name="trash" color={colors.text} />
</TouchableOpacity>
</View>
</View>
</View>
)
})
const styles = StyleSheet.create({
container: {
flexDirection: "row",
padding: 16,
borderBottomWidth: 1,
},
image: {
width: 80,
height: 80,
borderRadius: 8,
},
content: {
flex: 1,
marginLeft: 12,
justifyContent: "space-between",
},
info: {
flex: 1,
},
title: {
fontSize: 14,
fontWeight: "600",
marginBottom: 4,
},
variant: {
fontSize: 12,
marginBottom: 4,
},
price: {
fontSize: 14,
fontWeight: "700",
marginTop: 4,
},
actions: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: 8,
},
quantityContainer: {
flexDirection: "row",
alignItems: "center",
},
quantityButton: {
width: 28,
height: 28,
borderRadius: 14,
borderWidth: 1,
alignItems: "center",
justifyContent: "center",
},
quantityButtonText: {
fontSize: 16,
fontWeight: "600",
},
quantity: {
marginHorizontal: 12,
fontSize: 14,
fontWeight: "600",
minWidth: 20,
textAlign: "center",
},
removeButton: {
padding: 4,
},
})

The CartItem component receives the following props:

  • item: The cart line item to display.
  • currencyCode: The currency code for formatting prices.
  • onUpdateQuantity: Callback to update the quantity of the item.
  • onRemove: Callback to remove the item from the cart.

In the component, you display the item's image, title, variant name, and price. You also provide buttons to adjust the quantity and remove the item from the cart.

Add Cart Screen

Next, you'll implement the cart screen where users can view and manage their cart items.

As mentioned before, you'll have a tab for the cart screen. So, to create the cart screen, create the file app/(drawer)/(tabs)/(cart)/index.tsx with the following content:

tsx

tsxCopy
import { CartItem } from "@/components/cart-item"
import { Loading } from "@/components/loading"
import { Button } from "@/components/ui/button"
import { Colors } from "@/constants/theme"
import { useCart } from "@/context/cart-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import { formatPrice } from "@/lib/format-price"
import { useRouter } from "expo-router"
import React from "react"
import { FlatList, StyleSheet, Text, View } from "react-native"
export default function CartScreen() {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const router = useRouter()
const { cart, updateItemQuantity, removeItem, loading } = useCart()
const isEmpty = !cart?.items || cart.items.length === 0
if (loading && !cart) {
return <Loading message="Loading cart..." />
}
if (isEmpty) {
return (
<View style={[styles.emptyContainer, { backgroundColor: colors.background }]}>
<Text style={[styles.emptyTitle, { color: colors.text }]}>Your cart is empty</Text>
<Text style={[styles.emptyText, { color: colors.icon }]}>
Add some products to get started
</Text>
<Button
title="Browse Products"
onPress={() => router.push("/")}
style={styles.browseButton}
/>
</View>
)
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<FlatList
data={cart.items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<CartItem
item={item}
currencyCode={cart.currency_code}
onUpdateQuantity={(quantity) => updateItemQuantity(item.id, quantity)}
onRemove={() => removeItem(item.id)}
/>
)}
contentContainerStyle={styles.listContent}
/>
<View style={[styles.footer, { backgroundColor: colors.background, borderTopColor: colors.icon + "30" }]}>
<View style={styles.totals}>
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Subtotal</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.item_subtotal, cart.currency_code)}
</Text>
</View>
{cart.tax_total !== undefined && cart.tax_total > 0 && (
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Tax</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.tax_total, cart.currency_code)}
</Text>
</View>
)}
{cart.shipping_total !== undefined && cart.shipping_total > 0 && (
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Shipping</Text>
<Text style={[styles.totalValue, { color: colors.text }]}>
{formatPrice(cart.shipping_total, cart.currency_code)}
</Text>
</View>
)}
<View style={[styles.totalRow, styles.grandTotalRow, { borderTopColor: colors.border }]}>
<Text style={[styles.grandTotalLabel, { color: colors.text }]}>Total</Text>
<Text style={[styles.grandTotalValue, { color: colors.tint }]}>
{formatPrice(cart.total, cart.currency_code)}
</Text>
</View>
</View>
<Button
title="Proceed to Checkout"
onPress={() => {
// TODO navigate to checkout screen
}}
loading={loading}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
emptyContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 40,
},
emptyTitle: {
fontSize: 24,
fontWeight: "700",
marginBottom: 12,
},
emptyText: {
fontSize: 16,
textAlign: "center",
marginBottom: 32,
},
browseButton: {
minWidth: 200,
},
listContent: {
paddingBottom: 20,
},
footer: {
padding: 16,
borderTopWidth: 1,
},
totals: {
marginBottom: 20,
},
totalRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
},
totalLabel: {
fontSize: 14,
},
totalValue: {
fontSize: 14,
fontWeight: "500",
},
grandTotalRow: {
marginTop: 8,
paddingTop: 12,
borderTopWidth: 1,
},
grandTotalLabel: {
fontSize: 18,
fontWeight: "700",
},
grandTotalValue: {
fontSize: 20,
fontWeight: "700",
},
})

You define the CartScreen component that uses the useCart hook to access the cart state and actions. It handles three main states:

  • Loading: Displays a loading indicator while fetching the cart.
  • Empty Cart: Displays a message and a button to browse products if the cart is empty.
  • Cart with Items: Renders a list of cart items using the CartItem component, along with cart totals.

You also provide a button to proceed to checkout. Right now, the button does nothing. You'll change it to navigate to the checkout screen later.

Next, you'll add stack navigation for the cart screen.

Create the file app/(drawer)/(tabs)/(cart)/_layout.tsx with the following content:

tsx

tsxCopy
import { useColorScheme } from "@/hooks/use-color-scheme"
import { DrawerActions } from "@react-navigation/native"
import { Stack, useNavigation } from "expo-router"
import React from "react"
import { TouchableOpacity } from "react-native"
import { IconSymbol } from "@/components/ui/icon-symbol"
import { Colors } from "@/constants/theme"
export default function CartStackLayout() {
const colorScheme = useColorScheme()
const navigation = useNavigation()
const colors = Colors[colorScheme ?? "light"]
return (
<Stack
screenOptions={{
headerShown: true,
}}
>
<Stack.Screen
name="index"
options={{
title: "Cart",
headerLeft: () => (
<TouchableOpacity
onPress={() => navigation.dispatch(DrawerActions.openDrawer())}
style={{ height: 36, width: 36, display: "flex", alignItems: "center", justifyContent: "center" }}
>
<IconSymbol size={28} name="line.3.horizontal" color={colors.icon} />
</TouchableOpacity>
),
}}
/>
</Stack>
)
}

You show the cart screen with a header that includes a button to open the drawer navigation.

Add Cart Tab to Navigation

Finally, you'll add the cart tab to the main tab navigator.

In app/(drawer)/(tabs)/_layout.tsx, add the following import at the top of the file:

tsx

tsxCopy
import { useCart } from "@/context/cart-context"

Then, in the TabLayout component, add the following before the return statement:

tsx

tsxCopy
const { cart } = useCart()
const itemCount = cart?.items?.length || 0

You access the cart from the cart context to get the number of items in the cart.

Finally, replace the // TODO add cart tab comment in the return statement with the following:

tsx

tsxCopy
<Tabs.Screen
name="(cart)"
options={{
title: "Cart",
tabBarIcon: ({ color }) => <IconSymbol size={28} name="cart.fill" color={color} />,
tabBarBadge: itemCount > 0 ? itemCount : undefined,
tabBarBadgeStyle: {
backgroundColor: Colors[colorScheme ?? "light"].tint,
},
headerShown: false, // Let the cart stack manage its own headers
}}
/>

You add a new tab to the tab navigator for the cart screen. The tab displays a badge with the number of items in the cart.

Test Cart Screen

To test out the cart screen, run your Aiminaabee application and Expo app.

Then, add some products to your cart from the product detail screen. You can open the cart by clicking its tab in the bottom navigation.

Cart screen with items in it

You can update an item's quantity or remove an item, and the cart will be updated accordingly.


Next: 11. Checkout