11. Checkout

Collect email, address, shipping, and payment, then place the order.

On this page

In this step, you'll create a checkout screen where users enter their address, choose shipping and payment methods, and place their order.

The checkout screen will have three steps:

  1. Delivery: Enter shipping and billing addresses.
  2. Shipping: Choose a shipping method.
  3. Payment: Choose a payment method.

You'll implement first the three steps components, including their components and utilities, then the main checkout screen and navigation.

Add Address Form Component

First, you'll create an address form component to collect shipping and billing addresses.

Create the file components/checkout/address-form.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 type { HttpTypes } from "@aiminaabeejs/types"
import { Picker } from "@react-native-picker/picker"
import React, { useRef, useState } from "react"
import {
Modal,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native"
interface AddressFormProps {
firstName: string;
lastName: string;
address: string;
city: string;
postalCode: string;
countryCode: string;
phone: string;
countries: HttpTypes.StoreRegionCountry[];
onFirstNameChange: (value: string) => void;
onLastNameChange: (value: string) => void;
onAddressChange: (value: string) => void;
onCityChange: (value: string) => void;
onPostalCodeChange: (value: string) => void;
onCountryCodeChange: (value: string) => void;
onPhoneChange: (value: string) => void;
}
export function AddressForm({
firstName,
lastName,
address,
city,
postalCode,
countryCode,
phone,
countries,
onFirstNameChange,
onLastNameChange,
onAddressChange,
onCityChange,
onPostalCodeChange,
onCountryCodeChange,
onPhoneChange,
}: AddressFormProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const [showPicker, setShowPicker] = useState(false)
const [tempCountryCode, setTempCountryCode] = useState(countryCode)
// Create refs for each input field
const lastNameRef = useRef<TextInput>(null)
const addressRef = useRef<TextInput>(null)
const cityRef = useRef<TextInput>(null)
const postalCodeRef = useRef<TextInput>(null)
const phoneRef = useRef<TextInput>(null)
const selectedCountry = countries.find(
(c) => (c.iso_2 || c.id) === countryCode
)
const selectedCountryName = selectedCountry
? selectedCountry.display_name || selectedCountry.name || selectedCountry.iso_2 || selectedCountry.id
: "Select Country"
const handleDone = () => {
onCountryCodeChange(tempCountryCode)
setShowPicker(false)
// Focus phone field after country selection
setTimeout(() => phoneRef.current?.focus(), 100)
}
const handleCancel = () => {
setTempCountryCode(countryCode)
setShowPicker(false)
}
return (
<>
<View style={styles.row}>
<TextInput
style={[
styles.input,
styles.halfInput,
{ color: colors.text, borderColor: colors.icon + "30" },
]}
placeholder="First Name"
placeholderTextColor={colors.icon}
value={firstName}
onChangeText={onFirstNameChange}
returnKeyType="next"
onSubmitEditing={() => lastNameRef.current?.focus()}
/>
<TextInput
ref={lastNameRef}
style={[
styles.input,
styles.halfInput,
{ color: colors.text, borderColor: colors.icon + "30" },
]}
placeholder="Last Name"
placeholderTextColor={colors.icon}
value={lastName}
onChangeText={onLastNameChange}
returnKeyType="next"
onSubmitEditing={() => addressRef.current?.focus()}
/>
</View>
<TextInput
ref={addressRef}
style={[styles.input, { color: colors.text, borderColor: colors.icon + "30" }]}
placeholder="Address"
placeholderTextColor={colors.icon}
value={address}
onChangeText={onAddressChange}
returnKeyType="next"
onSubmitEditing={() => cityRef.current?.focus()}
/>
<View style={styles.row}>
<TextInput
ref={cityRef}
style={[
styles.input,
styles.halfInput,
{ color: colors.text, borderColor: colors.icon + "30" },
]}
placeholder="City"
placeholderTextColor={colors.icon}
value={city}
onChangeText={onCityChange}
returnKeyType="next"
onSubmitEditing={() => postalCodeRef.current?.focus()}
/>
<TextInput
ref={postalCodeRef}
style={[
styles.input,
styles.halfInput,
{ color: colors.text, borderColor: colors.icon + "30" },
]}
placeholder="Postal Code"
placeholderTextColor={colors.icon}
value={postalCode}
onChangeText={onPostalCodeChange}
returnKeyType="next"
onSubmitEditing={() => {
setTempCountryCode(countryCode)
setShowPicker(true)
}}
/>
</View>
<TouchableOpacity
style={[
styles.input,
styles.pickerButton,
{ borderColor: colors.icon + "30" },
]}
onPress={() => {
setTempCountryCode(countryCode)
setShowPicker(true)
}}
>
<Text style={[styles.pickerButtonText, { color: countryCode ? colors.text : colors.icon }]}>
{selectedCountryName}
</Text>
<IconSymbol size={20} name="chevron.down" color={colors.icon} />
</TouchableOpacity>
<Modal
visible={showPicker}
transparent={true}
animationType="none"
onRequestClose={handleCancel}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={handleCancel}
>
<TouchableOpacity
activeOpacity={1}
style={[styles.modalContent, { backgroundColor: colors.background }]}
onPress={(e) => e.stopPropagation()}
>
<View style={[styles.modalHeader, { borderBottomColor: colors.icon + "30" }]}>
<TouchableOpacity onPress={handleCancel}>
<Text style={[styles.modalButton, { color: colors.tint }]}>Cancel</Text>
</TouchableOpacity>
<Text style={[styles.modalTitle, { color: colors.text }]}>Select Country</Text>
<TouchableOpacity onPress={handleDone}>
<Text style={[styles.modalButton, { color: colors.tint }]}>Done</Text>
</TouchableOpacity>
</View>
<Picker
selectedValue={tempCountryCode}
onValueChange={(value) => {
setTempCountryCode(value)
if (Platform.OS === "android") {
onCountryCodeChange(value)
setShowPicker(false)
// Focus phone field after country selection on Android
setTimeout(() => phoneRef.current?.focus(), 100)
}
}}
style={[styles.picker, Platform.OS === "android" && { color: colors.text }]}
itemStyle={Platform.OS === "ios" ? styles.pickerItemIOS : undefined}
>
<Picker.Item label="Select Country" value="" enabled={false} />
{countries.map((country) => {
const code = country.iso_2 || country.id
const name = country.display_name || country.name || country.iso_2 || country.id
return <Picker.Item key={code} label={name} value={code} />
})}
</Picker>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
<TextInput
ref={phoneRef}
style={[styles.input, { color: colors.text, borderColor: colors.icon + "30" }]}
placeholder="Phone Number"
placeholderTextColor={colors.icon}
value={phone}
onChangeText={onPhoneChange}
keyboardType="phone-pad"
returnKeyType="done"
/>
</>
)
}
const styles = StyleSheet.create({
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 12,
},
row: {
flexDirection: "row",
gap: 12,
},
halfInput: {
flex: 1,
},
pickerButton: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
pickerButtonText: {
fontSize: 16,
flex: 1,
},
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContent: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: "50%",
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
},
modalTitle: {
fontSize: 18,
fontWeight: "600",
},
modalButton: {
fontSize: 16,
fontWeight: "600",
},
picker: {
width: "100%",
height: Platform.OS === "ios" ? 250 : 48,
},
pickerItemIOS: {
height: 200,
},
})

The AddressForm component receives various props for the address fields and their change handlers. It also receives a list of countries to populate the country picker.

The component renders input fields for the address and a modal picker for selecting the country. It handles input focus and country selection appropriately.

Add Delivery Step Component

Next, you'll create the delivery step component for the checkout process. It will use the AddressForm component to collect shipping and billing addresses.

Create the file components/checkout/delivery-step.tsx with the following content:

tsx

tsxCopy
import { AddressForm } from "@/components/checkout/address-form"
import { Button } from "@/components/ui/button"
import { Colors } from "@/constants/theme"
import { useRegion } from "@/context/region-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import React, { useEffect, useRef, useState } from "react"
import {
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
View,
} from "react-native"
interface Address {
firstName: string;
lastName: string;
address: string;
city: string;
postalCode: string;
countryCode: string;
phone: string;
}
interface DeliveryStepProps {
email: string;
shippingAddress: Address;
billingAddress: Address;
useSameForBilling: boolean;
loading: boolean;
onEmailChange: (value: string) => void;
onShippingAddressChange: (field: keyof Address, value: string) => void;
onBillingAddressChange: (field: keyof Address, value: string) => void;
onUseSameForBillingChange: (value: boolean) => void;
onNext: () => void;
}
export function DeliveryStep({
email,
shippingAddress,
billingAddress,
useSameForBilling,
loading,
onEmailChange,
onShippingAddressChange,
onBillingAddressChange,
onUseSameForBillingChange,
onNext,
}: DeliveryStepProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const { selectedRegion } = useRegion()
const scrollViewRef = useRef<ScrollView>(null)
const [isKeyboardVisible, setKeyboardVisible] = useState(false)
const countries = selectedRegion?.countries || []
useEffect(() => {
const keyboardWillShowListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
() => setKeyboardVisible(true)
)
const keyboardWillHideListener = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
() => setKeyboardVisible(false)
)
return () => {
keyboardWillShowListener.remove()
keyboardWillHideListener.remove()
}
}, [])
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}
>
<ScrollView
ref={scrollViewRef}
style={styles.scrollView}
contentContainerStyle={[
styles.scrollContent,
isKeyboardVisible && styles.scrollContentKeyboard,
]}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
showsVerticalScrollIndicator={true}
automaticallyAdjustKeyboardInsets={true}
>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>
Contact Information
</Text>
<TextInput
style={[styles.input, { color: colors.text, borderColor: colors.icon + "30" }]}
placeholder="Email"
placeholderTextColor={colors.icon}
value={email}
onChangeText={onEmailChange}
keyboardType="email-address"
autoCapitalize="none"
returnKeyType="next"
/>
<Text style={[styles.sectionTitle, { color: colors.text, marginTop: 20 }]}>
Shipping Address
</Text>
<AddressForm
firstName={shippingAddress.firstName}
lastName={shippingAddress.lastName}
address={shippingAddress.address}
city={shippingAddress.city}
postalCode={shippingAddress.postalCode}
countryCode={shippingAddress.countryCode}
phone={shippingAddress.phone}
countries={countries}
onFirstNameChange={(value) => onShippingAddressChange("firstName", value)}
onLastNameChange={(value) => onShippingAddressChange("lastName", value)}
onAddressChange={(value) => onShippingAddressChange("address", value)}
onCityChange={(value) => onShippingAddressChange("city", value)}
onPostalCodeChange={(value) => onShippingAddressChange("postalCode", value)}
onCountryCodeChange={(value) => onShippingAddressChange("countryCode", value)}
onPhoneChange={(value) => onShippingAddressChange("phone", value)}
/>
<View style={styles.switchContainer}>
<Text style={[styles.switchLabel, { color: colors.text }]}>
Use same address for billing
</Text>
<Switch
value={useSameForBilling}
onValueChange={onUseSameForBillingChange}
/>
</View>
{!useSameForBilling && (
<>
<Text style={[styles.sectionTitle, { color: colors.text, marginTop: 20 }]}>
Billing Address
</Text>
<AddressForm
firstName={billingAddress.firstName}
lastName={billingAddress.lastName}
address={billingAddress.address}
city={billingAddress.city}
postalCode={billingAddress.postalCode}
countryCode={billingAddress.countryCode}
phone={billingAddress.phone}
countries={countries}
onFirstNameChange={(value) => onBillingAddressChange("firstName", value)}
onLastNameChange={(value) => onBillingAddressChange("lastName", value)}
onAddressChange={(value) => onBillingAddressChange("address", value)}
onCityChange={(value) => onBillingAddressChange("city", value)}
onPostalCodeChange={(value) => onBillingAddressChange("postalCode", value)}
onCountryCodeChange={(value) => onBillingAddressChange("countryCode", value)}
onPhoneChange={(value) => onBillingAddressChange("phone", value)}
/>
</>
)}
</View>
{/* Button moved inside ScrollView for consistent behavior */}
<View style={[styles.buttonContainer, { backgroundColor: colors.background }]}>
<Button
title="Continue"
onPress={onNext}
loading={loading}
style={styles.button}
/>
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingBottom: 20,
},
scrollContentKeyboard: {
paddingBottom: Platform.OS === "ios" ? 300 : 320,
},
section: {
padding: 20,
},
buttonContainer: {
padding: 20,
paddingTop: 10,
paddingBottom: 30,
},
sectionTitle: {
fontSize: 20,
fontWeight: "700",
marginBottom: 20,
},
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginBottom: 12,
},
switchContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginVertical: 16,
},
switchLabel: {
fontSize: 16,
flex: 1,
},
button: {
marginTop: 8,
},
})

The DeliveryStep component receives the following props:

  • email: The email address associated with the cart.
  • shippingAddress: The shipping address details of the cart.
  • billingAddress: The billing address details of the cart.
  • useSameForBilling: A boolean indicating whether to use the shipping address for billing.
  • loading: A boolean indicating whether the form is in a loading state.
  • onEmailChange: Callback to update the email address.
  • onShippingAddressChange: Callback to update the shipping address fields.
  • onBillingAddressChange: Callback to update the billing address fields.
  • onUseSameForBillingChange: Callback to toggle the use of the same address for billing.
  • onNext: Callback to proceed to the next step.

The component renders an email input field and uses the AddressForm component to collect shipping and billing addresses. A switch is provided to toggle whether to use the same address for billing.

Add Shipping Step Component

Next, you'll create the shipping step component for the checkout process. This step allows users to select a shipping method.

Create the file components/checkout/shipping-step.tsx with the following content:

tsx

tsxCopy
import { Loading } from "@/components/loading"
import { Button } from "@/components/ui/button"
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 React from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
interface ShippingStepProps {
shippingOptions: HttpTypes.StoreCartShippingOption[];
selectedShippingOption: string | null;
currencyCode?: string;
loading: boolean;
onSelectOption: (optionId: string) => void;
onBack: () => void;
onNext: () => void;
}
export function ShippingStep({
shippingOptions,
selectedShippingOption,
currencyCode,
loading,
onSelectOption,
onBack,
onNext,
}: ShippingStepProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
return (
<View style={styles.container}>
<View style={styles.contentWrapper}>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>
Select Shipping Method
</Text>
{loading ? (
<Loading />
) : shippingOptions.length === 0 ? (
<Text style={[styles.emptyText, { color: colors.icon }]}>
No shipping options available
</Text>
) : (
shippingOptions.map((option) => (
<TouchableOpacity
key={option.id}
style={[
styles.optionCard,
{
backgroundColor:
selectedShippingOption === option.id
? colors.tint + "20"
: "transparent",
borderColor:
selectedShippingOption === option.id
? colors.tint
: colors.icon + "30",
},
]}
onPress={() => onSelectOption(option.id)}
>
<View style={styles.optionInfo}>
<Text
style={[
styles.optionTitle,
{
color:
selectedShippingOption === option.id
? colors.tint
: colors.text,
},
]}
>
{option.name}
</Text>
<Text style={[styles.optionPrice, { color: colors.text }]}>
{formatPrice(option.amount, currencyCode)}
</Text>
</View>
{selectedShippingOption === option.id && (
<Text style={{ color: colors.tint, fontSize: 20 }}>āœ“</Text>
)}
</TouchableOpacity>
))
)}
</View>
</View>
<View style={[styles.buttonContainer, { backgroundColor: colors.background, borderTopColor: colors.icon + "30" }]}>
<View style={styles.buttonRow}>
<Button
title="Back"
variant="secondary"
onPress={onBack}
style={styles.halfButton}
/>
<Button
title="Continue"
onPress={onNext}
loading={loading}
disabled={!selectedShippingOption}
style={styles.halfButton}
/>
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentWrapper: {
flex: 1,
},
section: {
padding: 20,
},
buttonContainer: {
padding: 20,
paddingBottom: 30,
borderTopWidth: 1,
},
sectionTitle: {
fontSize: 20,
fontWeight: "700",
marginBottom: 20,
},
optionCard: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderRadius: 8,
borderWidth: 1,
marginBottom: 12,
},
optionInfo: {
flex: 1,
},
optionTitle: {
fontSize: 16,
fontWeight: "600",
marginBottom: 4,
},
optionPrice: {
fontSize: 14,
},
emptyText: {
fontSize: 16,
textAlign: "center",
padding: 20,
},
buttonRow: {
flexDirection: "row",
gap: 12,
},
halfButton: {
flex: 1,
},
})

The ShippingStep component receives the following props:

  • shippingOptions: An array of available shipping options.
  • selectedShippingOption: The ID of the currently selected shipping option.
  • currencyCode: The currency code for formatting prices.
  • loading: A boolean indicating whether the shipping options are being loaded.
  • onSelectOption: Callback to select a shipping option.
  • onBack: Callback to go back to the previous step.
  • onNext: Callback to proceed to the next step.

The component renders a list of shipping options with their names and prices. The selected option is highlighted.

Add Payment Provider Utility

Before creating the payment step component, you'll add a utility function that maps payment provider IDs to their display information.

Create the file lib/payment-providers.ts with the following content:

ts

tsCopy
/**
* Information about a payment provider for display purposes
*/
export interface PaymentProviderInfo {
icon: string;
title: string;
}
/**
* Get display information for a payment provider based on its ID
* Returns an icon name and formatted title for the payment provider
*/
export function getPaymentProviderInfo(providerId: string): PaymentProviderInfo {
switch (providerId) {
case "pp_system_default":
return {
icon: "creditcard",
title: "Manual Payment",
}
default:
return {
icon: "creditcard",
title: providerId.replace("pp_", "").replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()),
}
}
}

The getPaymentProviderInfo function takes a payment provider ID and returns an object containing an icon name and a formatted title for display purposes.

The function only handles Aiminaabee's built-in manual payment provider pp_system_default. You can expand it later to include more providers that you integrate.

Add Payment Step Component

You'll now create the payment step component for the checkout process. This step allows users to select a payment method, and shows the cart's summary before placing the order.

This step will not handle actual payment processing. Instead, it will just allow users to select a payment method and place the order. If you integrate real payment providers later, you'll need to handle their specific payment flows.

Create the file components/checkout/payment-step.tsx with the following content:

tsx

tsxCopy
import { Loading } from "@/components/loading"
import { Button } from "@/components/ui/button"
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 { getPaymentProviderInfo } from "@/lib/payment-providers"
import type { HttpTypes } from "@aiminaabeejs/types"
import React from "react"
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"
interface PaymentStepProps {
cart: HttpTypes.StoreCart;
paymentProviders: HttpTypes.StorePaymentProvider[];
selectedPaymentProvider: string | null;
loading: boolean;
onSelectProvider: (providerId: string) => void;
onBack: () => void;
onPlaceOrder: () => void;
}
export function PaymentStep({
cart,
paymentProviders,
selectedPaymentProvider,
loading,
onSelectProvider,
onBack,
onPlaceOrder,
}: PaymentStepProps) {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
return (
<View style={styles.container}>
<View style={styles.contentWrapper}>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: colors.text }]}>
Select Payment Method
</Text>
{loading ? (
<Loading />
) : paymentProviders.length === 0 ? (
<Text style={[styles.emptyText, { color: colors.icon }]}>
No payment providers available
</Text>
) : (
paymentProviders.map((provider) => {
const providerInfo = getPaymentProviderInfo(provider.id)
const isSelected = selectedPaymentProvider === provider.id
return (
<TouchableOpacity
key={provider.id}
style={[
styles.optionCard,
{
backgroundColor: isSelected ? colors.tint + "20" : "transparent",
borderColor: isSelected ? colors.tint : colors.icon + "30",
},
]}
onPress={() => onSelectProvider(provider.id)}
>
<View style={styles.optionContent}>
<IconSymbol
size={24}
name={providerInfo.icon as any}
color={isSelected ? colors.tint : colors.icon}
/>
<Text
style={[
styles.optionTitle,
{
color: isSelected ? colors.tint : colors.text,
},
]}
>
{providerInfo.title}
</Text>
</View>
{isSelected && (
<Text style={{ color: colors.tint, fontSize: 20 }}>āœ“</Text>
)}
</TouchableOpacity>
)
})
)}
<View style={[styles.summary, { borderColor: colors.icon + "30" }]}>
<Text style={[styles.summaryTitle, { color: colors.text }]}>
Order Summary
</Text>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: colors.text }]}>Subtotal</Text>
<Text style={[styles.summaryValue, { color: colors.text }]}>
{formatPrice(cart.item_subtotal || 0, cart.currency_code)}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: colors.text }]}>Discount</Text>
<Text style={[styles.summaryValue, { color: colors.text }]}>
{(cart.discount_total || 0) > 0 ? "-" : ""}{formatPrice(cart.discount_total || 0, cart.currency_code)}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: colors.text }]}>Shipping</Text>
<Text style={[styles.summaryValue, { color: colors.text }]}>
{formatPrice(cart.shipping_total || 0, cart.currency_code)}
</Text>
</View>
<View style={styles.summaryRow}>
<Text style={[styles.summaryLabel, { color: colors.text }]}>Tax</Text>
<Text style={[styles.summaryValue, { color: colors.text }]}>
{formatPrice(cart.tax_total || 0, cart.currency_code)}
</Text>
</View>
<View style={[styles.summaryRow, styles.totalRow, { borderTopColor: colors.border }]}>
<Text style={[styles.totalLabel, { color: colors.text }]}>Total</Text>
<Text style={[styles.totalValue, { color: colors.tint }]}>
{formatPrice(cart.total, cart.currency_code)}
</Text>
</View>
</View>
</View>
</View>
<View style={[styles.buttonContainer, { backgroundColor: colors.background, borderTopColor: colors.icon + "30" }]}>
<View style={styles.buttonRow}>
<Button
title="Back"
variant="secondary"
onPress={onBack}
style={styles.halfButton}
/>
<Button
title="Place Order"
onPress={onPlaceOrder}
loading={loading}
disabled={!selectedPaymentProvider}
style={styles.halfButton}
/>
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentWrapper: {
flex: 1,
},
section: {
padding: 20,
},
buttonContainer: {
padding: 20,
paddingBottom: 30,
borderTopWidth: 1,
},
sectionTitle: {
fontSize: 20,
fontWeight: "700",
marginBottom: 20,
},
optionCard: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderRadius: 8,
borderWidth: 1,
marginBottom: 12,
},
optionContent: {
flexDirection: "row",
alignItems: "center",
gap: 12,
flex: 1,
},
optionTitle: {
fontSize: 16,
fontWeight: "600",
},
emptyText: {
fontSize: 16,
textAlign: "center",
padding: 20,
},
summary: {
borderWidth: 1,
borderRadius: 8,
padding: 16,
marginTop: 20,
marginBottom: 20,
},
summaryTitle: {
fontSize: 18,
fontWeight: "700",
marginBottom: 12,
},
summaryRow: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 8,
},
summaryLabel: {
fontSize: 14,
},
summaryValue: {
fontSize: 14,
fontWeight: "500",
},
totalRow: {
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
},
totalLabel: {
fontSize: 18,
fontWeight: "700",
},
totalValue: {
fontSize: 20,
fontWeight: "700",
},
buttonRow: {
flexDirection: "row",
gap: 12,
},
halfButton: {
flex: 1,
},
})

The PaymentStep component receives the following props:

  • cart: The current cart object containing pricing details.
  • paymentProviders: An array of available payment providers.
  • selectedPaymentProvider: The ID of the currently selected payment provider.
  • loading: A boolean indicating whether the payment providers are being loaded.
  • onSelectProvider: Callback to select a payment provider.
  • onBack: Callback to go back to the previous step.
  • onPlaceOrder: Callback to place the order.

The component renders a list of payment providers with their icons and titles. The selected provider is highlighted.

A summary of the cart's pricing details is displayed, including subtotal, discounts, shipping, tax, and total amount.

Add Checkout Screen Component

Finally, you'll create the main checkout screen that manages the multi-step checkout process. It will use the previously created step components to guide users through entering their information, selecting shipping and payment methods, and placing their order.

Create the file app/checkout.tsx with the following content:

tsx

tsxCopy
import { DeliveryStep } from "@/components/checkout/delivery-step"
import { PaymentStep } from "@/components/checkout/payment-step"
import { ShippingStep } from "@/components/checkout/shipping-step"
import { Colors } from "@/constants/theme"
import { useCart } from "@/context/cart-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import { sdk } from "@/lib/sdk"
import type { HttpTypes } from "@aiminaabeejs/types"
import React, { useCallback, useEffect, useState } from "react"
import { Alert, StyleSheet, Text, View } from "react-native"
type CheckoutStep = "delivery" | "shipping" | "payment";
export default function CheckoutScreen() {
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
const { cart, refreshCart } = useCart()
const [currentStep, setCurrentStep] = useState<CheckoutStep>("delivery")
const [loading, setLoading] = useState(false)
// Contact & Address state
const [email, setEmail] = useState("")
const [shippingAddress, setShippingAddress] = useState({
firstName: "",
lastName: "",
address: "",
city: "",
postalCode: "",
countryCode: "",
phone: "",
})
const [useSameForBilling, setUseSameForBilling] = useState(true)
const [billingAddress, setBillingAddress] = useState({
firstName: "",
lastName: "",
address: "",
city: "",
postalCode: "",
countryCode: "",
phone: "",
})
// Shipping step
const [shippingOptions, setShippingOptions] = useState<HttpTypes.StoreCartShippingOption[]>([])
const [selectedShippingOption, setSelectedShippingOption] = useState<string | null>(null)
// Payment step
const [paymentProviders, setPaymentProviders] = useState<HttpTypes.StorePaymentProvider[]>([])
const [selectedPaymentProvider, setSelectedPaymentProvider] = useState<string | null>(null)
// TODO update state when cart changes
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
centerContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
steps: {
flexDirection: "row",
justifyContent: "space-around",
padding: 20,
borderBottomWidth: 1,
},
stepIndicator: {
alignItems: "center",
},
stepCircle: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
marginBottom: 8,
},
stepNumber: {
fontSize: 16,
fontWeight: "600",
},
stepLabel: {
fontSize: 12,
},
content: {
flex: 1,
},
errorText: {
fontSize: 16,
textAlign: "center",
},
})

The CheckoutScreen component includes the following state variables:

  • currentStep: The current step in the checkout process.
  • loading: A boolean indicating whether an operation is in progress.
  • Contact and address fields for shipping and billing.
  • Shipping options and the selected shipping option.
  • Payment providers and the selected payment provider.

Next, you'll handle updating the state variables when the cart changes. Replace the // TODO update state when cart changes comment with the following:

tsx

tsxCopy
useEffect(() => {
// Populate form with existing cart data or reset to empty values
setEmail(cart?.email || "")
setShippingAddress({
firstName: cart?.shipping_address?.first_name || "",
lastName: cart?.shipping_address?.last_name || "",
address: cart?.shipping_address?.address_1 || "",
city: cart?.shipping_address?.city || "",
postalCode: cart?.shipping_address?.postal_code || "",
countryCode: cart?.shipping_address?.country_code || "",
phone: cart?.shipping_address?.phone || "",
})
// Billing address - check if different from shipping
const hasDifferentBilling = cart?.billing_address &&
(cart.billing_address.address_1 !== cart.shipping_address?.address_1 ||
cart.billing_address.city !== cart.shipping_address?.city)
setUseSameForBilling(!hasDifferentBilling)
setBillingAddress({
firstName: cart?.billing_address?.first_name || "",
lastName: cart?.billing_address?.last_name || "",
address: cart?.billing_address?.address_1 || "",
city: cart?.billing_address?.city || "",
postalCode: cart?.billing_address?.postal_code || "",
countryCode: cart?.billing_address?.country_code || "",
phone: cart?.billing_address?.phone || "",
})
// Reset selections when cart is null
if (!cart) {
setSelectedShippingOption(null)
setSelectedPaymentProvider(null)
setCurrentStep("delivery")
}
}, [cart])
// TODO fetch shipping options and payment providers

A useEffect hook populates the form fields with existing cart data when the cart changes.

Next, you'll fetch the available shipping options and payment providers when the user reaches the respective steps. Replace the // TODO fetch shipping options and payment providers comment with the following:

tsx

tsxCopy
const fetchShippingOptions = useCallback(async () => {
if (!cart) {return}
try {
setLoading(true)
const { shipping_options } = await sdk.store.fulfillment.listCartOptions("us", {
cart_id: cart.id,
})
setShippingOptions(shipping_options || [])
} catch (err) {
console.error("Failed to fetch shipping options:", err)
Alert.alert("Error", "Failed to load shipping options")
} finally {
setLoading(false)
}
}, [cart])
const fetchPaymentProviders = useCallback(async () => {
if (!cart) {return}
try {
setLoading(true)
const { payment_providers } = await sdk.store.payment.listPaymentProviders("us", {
region_id: cart.region_id || "",
})
setPaymentProviders(payment_providers || [])
} catch (err) {
console.error("Failed to fetch payment providers:", err)
Alert.alert("Error", "Failed to load payment providers")
} finally {
setLoading(false)
}
}, [cart])
useEffect(() => {
if (currentStep === "shipping") {
fetchShippingOptions()
} else if (currentStep === "payment") {
fetchPaymentProviders()
}
}, [currentStep, fetchShippingOptions, fetchPaymentProviders])
// TODO handle step transitions and order placement

You add a fetchShippingOptions function that retrieves shipping options, and a fetchPaymentProviders function that retrieves payment providers from the Aiminaabee backend.

You also add a useEffect hook that calls these functions when the user navigates to the shipping or payment steps.

Next, you'll add functions that handle moving between steps and placing the order. Replace the // TODO handle step transitions and order placement comment with the following:

tsx

tsxCopy
const handleDeliveryNext = async () => {
// Validate shipping address
if (!email || !shippingAddress.firstName || !shippingAddress.lastName ||
!shippingAddress.address || !shippingAddress.city || !shippingAddress.postalCode ||
!shippingAddress.countryCode || !shippingAddress.phone) {
Alert.alert("Error", "Please fill in all shipping address fields")
return
}
// Validate billing address if different
if (!useSameForBilling) {
if (!billingAddress.firstName || !billingAddress.lastName || !billingAddress.address ||
!billingAddress.city || !billingAddress.postalCode || !billingAddress.countryCode ||
!billingAddress.phone) {
Alert.alert("Error", "Please fill in all billing address fields")
return
}
}
if (!cart) {return}
try {
setLoading(true)
const shippingAddressData = {
first_name: shippingAddress.firstName,
last_name: shippingAddress.lastName,
address_1: shippingAddress.address,
city: shippingAddress.city,
postal_code: shippingAddress.postalCode,
country_code: shippingAddress.countryCode,
phone: shippingAddress.phone,
}
const billingAddressData = useSameForBilling ? shippingAddressData : {
first_name: billingAddress.firstName,
last_name: billingAddress.lastName,
address_1: billingAddress.address,
city: billingAddress.city,
postal_code: billingAddress.postalCode,
country_code: billingAddress.countryCode,
phone: billingAddress.phone,
}
await sdk.store.cart.update("us", cart.id, {
email,
shipping_address: shippingAddressData,
billing_address: billingAddressData,
})
await refreshCart()
setCurrentStep("shipping")
} catch (err) {
console.error("Failed to update cart:", err)
Alert.alert("Error", "Failed to save delivery information")
} finally {
setLoading(false)
}
}
const handleShippingNext = async () => {
if (!selectedShippingOption || !cart) {
Alert.alert("Error", "Please select a shipping method")
return
}
try {
setLoading(true)
await sdk.store.cart.addShippingMethod("us", cart.id, {
option_id: selectedShippingOption,
})
await refreshCart()
setCurrentStep("payment")
} catch (err) {
console.error("Failed to add shipping method:", err)
Alert.alert("Error", "Failed to save shipping method")
} finally {
setLoading(false)
}
}
const handlePlaceOrder = async () => {
if (!selectedPaymentProvider || !cart) {
Alert.alert("Error", "Please select a payment provider")
return
}
try {
setLoading(true)
// Create payment session
await sdk.store.payment.initiatePaymentSession("us", cart, {
provider_id: selectedPaymentProvider,
})
// Complete cart (converts cart to order on backend)
const result = await sdk.store.cart.complete("us", cart.id)
if (result.type === "order") {
// Navigate to order confirmation first
// Cart will be cleared on the order confirmation page to prevent empty cart flash
// TODO navigate to order confirmation screen with order details
} else {
Alert.alert("Error", result.error?.message || "Failed to complete order")
}
} catch (err: any) {
console.error("Failed to complete order:", err)
Alert.alert("Error", err?.message || "Failed to complete order")
} finally {
setLoading(false)
}
}
// TODO render step components

The following functions are added:

  • handleDeliveryNext: Validates the delivery information, updates the cart with the email and addresses, and moves to the shipping step.
  • handleShippingNext: Validates the selected shipping option, adds it to the cart, and moves to the payment step.
  • handlePlaceOrder: Validates the selected payment provider, initiates the payment session, completes the cart, and handles the order confirmation. This function should also navigate to an order confirmation screen, which you'll implement later.

Finally, you'll render the appropriate step component based on the current step. Replace the // TODO render step components comment with the following:

tsx

tsxCopy
if (!cart) {
return (
<View style={[styles.centerContainer, { backgroundColor: colors.background }]}>
<Text style={[styles.errorText, { color: colors.text }]}>
No cart found. Please add items to your cart first.
</Text>
</View>
)
}
// Active step uses inverted colors: white bg with dark text in dark mode, tint bg with white text in light mode
const activeStepBg = colorScheme === "dark" ? "#fff" : colors.tint
const activeStepText = colorScheme === "dark" ? "#000" : "#fff"
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View style={[styles.steps, { borderBottomColor: colors.border }]}>
{(["delivery", "shipping", "payment"] as CheckoutStep[]).map((step, index) => (
<View key={step} style={styles.stepIndicator}>
<View
style={[
styles.stepCircle,
{
backgroundColor:
currentStep === step ? activeStepBg : colors.icon + "30",
},
]}
>
<Text
style={[
styles.stepNumber,
{ color: currentStep === step ? activeStepText : colors.icon },
]}
>
{index + 1}
</Text>
</View>
<Text
style={[
styles.stepLabel,
{
color: currentStep === step ? colors.text : colors.icon,
fontWeight: currentStep === step ? "600" : "400",
},
]}
>
{step.charAt(0).toUpperCase() + step.slice(1)}
</Text>
</View>
))}
</View>
<View style={styles.content}>
{currentStep === "delivery" && (
<DeliveryStep
email={email}
shippingAddress={shippingAddress}
billingAddress={billingAddress}
useSameForBilling={useSameForBilling}
loading={loading}
onEmailChange={setEmail}
onShippingAddressChange={(field, value) =>
setShippingAddress((prev) => ({ ...prev, [field]: value }))
}
onBillingAddressChange={(field, value) =>
setBillingAddress((prev) => ({ ...prev, [field]: value }))
}
onUseSameForBillingChange={setUseSameForBilling}
onNext={handleDeliveryNext}
/>
)}
{currentStep === "shipping" && (
<ShippingStep
shippingOptions={shippingOptions}
selectedShippingOption={selectedShippingOption}
currencyCode={cart.currency_code}
loading={loading}
onSelectOption={setSelectedShippingOption}
onBack={() => setCurrentStep("delivery")}
onNext={handleShippingNext}
/>
)}
{currentStep === "payment" && (
<PaymentStep
cart={cart}
paymentProviders={paymentProviders}
selectedPaymentProvider={selectedPaymentProvider}
loading={loading}
onSelectProvider={setSelectedPaymentProvider}
onBack={() => setCurrentStep("shipping")}
onPlaceOrder={handlePlaceOrder}
/>
)}
</View>
</View>
)

If the cart isn't set, a message prompts the user to add items to their cart.

Otherwise, the step indicators are rendered at the top of the screen, and the current step's component is shown.

Add Checkout Screen to Navigation

Next, you'll add the checkout screen to the app's navigation structure. You'll add it as a top-level stack screen.

In app/_layout.tsx, Replace the TODO: Add checkout and order confirmation screens comment with the following:

tsx

tsxCopy
<Stack.Screen
name="checkout"
options={{
headerShown: true,
title: "Checkout",
presentation: "card",
headerBackButtonDisplayMode: "minimal",
}}
/>

Update Cart Screen to Navigate to Checkout

Finally, you'll update the cart screen to navigate to the checkout screen when the user taps the "Proceed to Checkout" button.

In app/(drawer)/(tabs)/(cart)/index.tsx, find the Button at the end of the CartScreen component's return statement, and change the onPress prop to the following:

tsx

tsxCopy
<Button
title="Proceed to Checkout"
onPress={() => router.push("/checkout")}
loading={loading}
/>

If you get a type error regarding the /checkout route, the error will be resolved when you run the app.

Test the Checkout Flow

To test the checkout flow, run your Aiminaabee application and Expo app.

Then, in your Expo app, click on the "Proceed to Checkout" button in the cart screen. You should be taken to the checkout screen with the Delivery step as the current step.

In the Delivery step, fill in the email and address fields, then click "Continue" to proceed to the Shipping step.

Delivery step with address filled

Then, in the Shipping step, select a shipping method and click "Continue" to proceed to the Payment step.

Shipping step with shipping method selected

Finally, in the Payment step, select a payment provider. You currently can place an order, but there is no order confirmation screen yet. You'll add it in the next step.

Payment step with payment provider selected


Next: 12. Order confirmation