6. Region selector

Let customers pick a region and persist it in the app.

On this page

In this step, you'll create a region selector component that allows users to select their country and currency. This is important for providing a localized shopping experience.

To implement this, you'll:

  1. Create a region context to manage the selected region state.
  2. Create a region selector component that allows users to choose their country and currency from a list.
  3. Update the app's navigation to add a drawer menu for the region selector.

Create Region Context

The region context will manage the selected region state and provide it to child components. You'll create a context, a provider component, and a custom hook to access the context.

To create the region context, create the file context/region-context.tsx with the following content:

tsx

tsxCopy
import { sdk } from "@/lib/sdk"
import type { HttpTypes } from "@aiminaabeejs/types"
import AsyncStorage from "@react-native-async-storage/async-storage"
import React, { createContext, ReactNode, useContext, useEffect, useState } from "react"
interface RegionContextType {
regions: HttpTypes.StoreRegion[];
selectedRegion: HttpTypes.StoreRegion | null;
selectedCountryCode: string | null;
setSelectedRegion: (region: HttpTypes.StoreRegion, countryCode: string) => void;
loading: boolean;
error: string | null;
}
const RegionContext = createContext<RegionContextType | undefined>(undefined)
const REGION_STORAGE_KEY = "selected_region_id"
const COUNTRY_STORAGE_KEY = "selected_country_code"
export function RegionProvider({ children }: { children: ReactNode }) {
const [regions, setRegions] = useState<HttpTypes.StoreRegion[]>([])
const [selectedRegion, setSelectedRegionState] = useState<HttpTypes.StoreRegion | null>(null)
const [selectedCountryCode, setSelectedCountryCode] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// TODO load and select regions
}
// TODO add useRegion hook

You define the RegionContext with the following properties that child components can use:

  1. regions: An array of available regions fetched from the Aiminaabee backend.
  2. selectedRegion: The currently selected region.
  3. selectedCountryCode: The country code of the selected country within the selected region. This is useful as regions can have multiple countries, and you'll allow customers to select their country.
  4. setSelectedRegion: A function to update the selected region and country code.
  5. loading: A boolean indicating whether the regions are being loaded.
  6. error: An error message if loading the regions fails.

You also define the RegionProvider component that will wrap the app and provide the region context to its children.

Next, you'll implement the logic to load the regions from the Aiminaabee backend, and manage the selected region state.

Replace the // TODO load and select regions comment with the following:

tsx

tsxCopy
const loadRegions = async () => {
try {
setLoading(true)
setError(null)
const { regions: fetchedRegions } = await sdk.store.region.list("us")
setRegions(fetchedRegions)
// Load saved region and country or use first region's first country
const savedRegionId = await AsyncStorage.getItem(REGION_STORAGE_KEY)
const savedCountryCode = await AsyncStorage.getItem(COUNTRY_STORAGE_KEY)
const regionToSelect = savedRegionId
? fetchedRegions.find((r) => r.id === savedRegionId) || fetchedRegions[0]
: fetchedRegions[0]
if (regionToSelect) {
setSelectedRegionState(regionToSelect)
await AsyncStorage.setItem(REGION_STORAGE_KEY, regionToSelect.id)
// Set country code - use saved one if it exists in the region, otherwise use first country
const countryCodeToSelect = savedCountryCode &&
regionToSelect.countries?.some((c) => (c.iso_2 || c.id) === savedCountryCode)
? savedCountryCode
: regionToSelect.countries?.[0]?.iso_2 || regionToSelect.countries?.[0]?.id || null
setSelectedCountryCode(countryCodeToSelect)
if (countryCodeToSelect) {
await AsyncStorage.setItem(COUNTRY_STORAGE_KEY, countryCodeToSelect)
}
}
} catch (err) {
console.error("Failed to load regions:", err)
setError("Failed to load regions. Please try again.")
} finally {
setLoading(false)
}
}
// Load regions on mount
useEffect(() => {
loadRegions()
}, [])
const setSelectedRegion = async (region: HttpTypes.StoreRegion, countryCode: string) => {
setSelectedRegionState(region)
setSelectedCountryCode(countryCode)
await AsyncStorage.setItem(REGION_STORAGE_KEY, region.id)
await AsyncStorage.setItem(COUNTRY_STORAGE_KEY, countryCode)
}
return (
<RegionContext.Provider
value={{
regions,
selectedRegion,
selectedCountryCode,
setSelectedRegion,
loading,
error,
}}
>
{children}
</RegionContext.Provider>
)

You define the loadRegions function that fetches the regions from the Aiminaabee backend using the JS SDK. It also loads any previously selected region and country from AsyncStorage, or defaults to the first region and its first country.

Then, you run the loadRegions function when the component mounts using the useEffect hook.

You also define the setSelectedRegion function that updates the selected region and country, and saves them to AsyncStorage.

Then, you provide the context values to child components using the RegionContext.Provider.

Finally, you'll create a custom hook in the same file to access the region context easily. Replace the // TODO add useRegion hook comment with the following:

tsx

tsxCopy
export function useRegion() {
const context = useContext(RegionContext)
if (!context) {
throw new Error("useRegion must be used within a RegionProvider")
}
return context
}

You define the useRegion hook that retrieves the context value using useContext. It also throws an error if the hook is used by a component that is not wrapped in the RegionProvider.

Create Region Selector Component

Next, you'll create the region selector component that allows users to choose their country and currency.

To create the region selector component, create the file components/region-selector.tsx with the following content:

tsx

tsxCopy
import { Colors } from "@/constants/theme"
import { useRegion } from "@/context/region-context"
import { useColorScheme } from "@/hooks/use-color-scheme"
import type { HttpTypes } from "@aiminaabeejs/types"
import React, { useMemo } from "react"
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native"
interface RegionSelectorProps {
onRegionChange?: () => void;
}
interface CountryWithRegion {
countryCode: string;
countryName: string;
region: HttpTypes.StoreRegion;
currencyCode: string;
}
export function RegionSelector({ onRegionChange }: RegionSelectorProps) {
const {
regions,
selectedRegion,
selectedCountryCode,
setSelectedRegion,
} = useRegion()
const colorScheme = useColorScheme()
const colors = Colors[colorScheme ?? "light"]
// Flatten countries from all regions
const countries = useMemo(() => {
const countryList: CountryWithRegion[] = []
regions.forEach((region) => {
if (region.countries) {
region.countries.forEach((country) => {
countryList.push({
countryCode: country.iso_2 || country.id,
countryName: country.display_name || country.name || country.iso_2 || country.id,
region: region,
currencyCode: region.currency_code || "",
})
})
}
})
// Sort alphabetically by country name
return countryList.sort((a, b) => a.countryName.localeCompare(b.countryName))
}, [regions])
// TODO handle country selection
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 20,
fontWeight: "700",
marginBottom: 20,
},
countryItem: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 16,
borderRadius: 8,
borderWidth: 1,
marginBottom: 8,
},
countryInfo: {
flex: 1,
},
countryName: {
fontSize: 16,
marginBottom: 4,
},
currencyCode: {
fontSize: 12,
},
emptyText: {
fontSize: 14,
textAlign: "center",
marginTop: 20,
},
})

You define the RegionSelector component that receives an optional onRegionChange prop. This prop is a callback function that will be called when the user selects a new region.

In the component, you:

  1. Use the useRegion hook to access all regions, the selected region and country, and the function to set the selected region.
  2. Use the useColorScheme hook to get the current color scheme and apply the appropriate colors.
  3. Create a memoized list of countries by flattening the countries from all regions. Each country includes its associated region and currency code. The list is sorted alphabetically by country name.

Next, you'll implement the logic to handle country selection and render the list of countries. Replace the // TODO handle country selection comment with the following:

tsx

tsxCopy
const handleSelectCountry = async (countryWithRegion: CountryWithRegion) => {
setSelectedRegion(countryWithRegion.region, countryWithRegion.countryCode)
onRegionChange?.()
}
const isCountrySelected = (countryWithRegion: CountryWithRegion) => {
return selectedRegion?.id === countryWithRegion.region.id &&
selectedCountryCode === countryWithRegion.countryCode
}
return (
<ScrollView style={styles.container}>
<Text style={[styles.title, { color: colors.text }]}>Select Country</Text>
{countries.length === 0 ? (
<Text style={[styles.emptyText, { color: colors.icon }]}>
No countries available
</Text>
) : (
countries.map((country) => {
const isSelected = isCountrySelected(country)
return (
<TouchableOpacity
key={`${country.region.id}-${country.countryCode}`}
style={[
styles.countryItem,
{
backgroundColor: isSelected ? colors.tint + "20" : "transparent",
borderColor: colors.icon + "30",
},
]}
onPress={() => handleSelectCountry(country)}
>
<View style={styles.countryInfo}>
<Text
style={[
styles.countryName,
{
color: isSelected ? colors.tint : colors.text,
fontWeight: isSelected ? "600" : "400",
},
]}
>
{country.countryName}
</Text>
<Text style={[styles.currencyCode, { color: colors.icon }]}>
{country.currencyCode.toUpperCase()}
</Text>
</View>
{isSelected && (
<Text style={{ color: colors.tint, fontSize: 18 }}>✓</Text>
)}
</TouchableOpacity>
)
})
)}
</ScrollView>
)

You define the handleSelectCountry function that is called when a user selects a country. It updates the selected region and country using the setSelectedRegion function from the context, and calls the onRegionChange callback if provided.

You also define the isCountrySelected function that checks if a given country is currently selected.

Finally, you render the countries in a scrollable view. For each country, you create a touchable item that displays the country name and currency code. The selected country is highlighted, and a checkmark is shown next to it.

Add Drawer Content Component

Next, you'll create a custom drawer content component that includes the region selector in the app's navigation drawer.

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

tsx

tsxCopy
import { DrawerContentComponentProps, DrawerContentScrollView } from "@react-navigation/drawer"
import React from "react"
import { StyleSheet, View } from "react-native"
import { RegionSelector } from "./region-selector"
export function DrawerContent(props: DrawerContentComponentProps) {
return (
<DrawerContentScrollView {...props}>
<View style={styles.container}>
<RegionSelector onRegionChange={() => props.navigation.closeDrawer()} />
</View>
</DrawerContentScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
})

The DrawerContent component receives the drawer navigation props. You render the RegionSelector component and pass an onRegionChange callback that closes the drawer when a region is selected.

Add Drawer Navigation

Finally, you'll update the app's navigation to wrap the main screens in a drawer navigator that uses the custom drawer content.

If you have an app/(tabs) directory for tab navigation, remove it for now. You'll add it later inside the drawer navigator.

Then, create the directory app/(drawer) which will contain the drawer navigation setup.

Next, create the file app/(drawer)/_layout.tsx with the following content:

tsx

tsxCopy
import { Drawer } from "expo-router/drawer"
import { DrawerContent } from "@/components/drawer-content"
export default function DrawerLayout() {
return (
<Drawer
drawerContent={(props) => <DrawerContent {...props} />}
screenOptions={{
headerShown: false,
drawerPosition: "left",
}}
>
{/* TODO add tabs screens */}
</Drawer>
)
}

The DrawerLayout component sets up the drawer navigator using Expo Router's Drawer component.

The custom drawer content is shown on the left side of the screen, and the header is hidden.

Later, you'll add the tab screens inside the drawer navigator.

Next, replace the content of the file app/_layout.tsx with the following:

tsx

tsxCopy
import { useColorScheme } from "@/hooks/use-color-scheme"
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native"
import { Stack } from "expo-router"
import { StatusBar } from "expo-status-bar"
import { GestureHandlerRootView } from "react-native-gesture-handler"
import "react-native-reanimated"
import { RegionProvider } from "@/context/region-context"
export default function RootLayout() {
const colorScheme = useColorScheme()
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<RegionProvider>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
{/* TODO: Add checkout and order confirmation screens */}
</Stack>
<StatusBar style="auto" />
</RegionProvider>
</ThemeProvider>
</GestureHandlerRootView>
)
}

The key changes are:

  1. Wrap the app in the RegionProvider to provide the region context to all components.
  2. Set the initial screen to the (drawer) layout, which contains the drawer navigator.

Later, you'll add the checkout and order confirmation screens to the stack navigator.

You'll test the region selector after implementing the main screens of the app in the next steps.


Next: 7. Cart context