A fully functional address picker with:
- πΊοΈ Interactive Google Map
- π Tap to select location
- π― Current GPS location button
- π Search with autocomplete
- π Auto-fill address details
Prerequisites
You need:
- Node.js (version 18+)
- Expo CLI:
npm install -g expo-cli - Google Cloud Account (free tier)
- Basic React knowledge (
useState,useEffect)
π Part 1: Project Setup
Step 1: Get Your Google Maps API Key
This is crucial! Without this, your map won't work.
1.1 Create a Google Cloud Project
- Go to Google Cloud Console
- Click the project dropdown at the top
- Click "New Project"
- Name it:
my-address-picker-app - Click "Create"
- Wait for the notification that your project is created
1.2 Enable Required APIs
You need to enable THREE APIs:
- In the search bar, type "Maps SDK for Android"
- Click on it β Click "Enable"
- Wait for it to enable (takes ~30 seconds)
Repeat for:
- Maps SDK for iOS
- Places API
- Geocoding API
Why do we need these?
- Maps SDK: Display the map
- Places API: Search for locations
- Geocoding API: Convert coordinates to addresses
1.3 Create API Key
- Go to "APIs & Services" β "Credentials"
- Click "Create Credentials" β "API Key"
- Copy your API key (looks like:
AIzaSyB3daWjbihjieRz7vrxl6-ok82013WGTabsjE) - IMPORTANT: Click "Restrict Key" to secure it
- Under "API restrictions", select:
- Maps SDK for Android
- Maps SDK for iOS
- Places API
- Geocoding API
- Click "Save"
1.4 Enable Billing
Google Maps requires billing info (but has a generous free tier):
- Go to "Billing" in the menu
- Link a billing account
- Don't worry: You get $200 free credit per month
- This tutorial's usage will likely cost $0
β οΈ Common Mistake: Skipping billing setup will cause "API key not valid" errors!
Step 2: Create Your Expo Project
Open your terminal and run:
# Create a new Expo project
npx create-expo-app my-address-picker --template blank
# Navigate into the project
cd my-address-picker
# Start the development server (to test everything works)
npx expo start
What's happening here?
-
create-expo-app: Creates a new React Native project using Expo -
--template blank: Starts with a minimal setup -
expo start: Launches the development server
You should see a QR code in your terminal. Scan it with Expo Go app to see the default "Hello World" screen.
β Checkpoint: If you see "Hello World" on your phone, you're good to go!
Step 3: Install Required Dependencies
Stop the server (Ctrl+C) and install packages:
npm install react-native-maps expo-location react-native-svg react-native-safe-area-context react-native-toast-message
What each package does:
| Package | Purpose | Why We Need It |
|---|---|---|
react-native-maps |
Display interactive maps | Core feature - shows the map |
expo-location |
Access device GPS | Get user's current location |
react-native-svg |
Render custom icons | Beautiful marker and button icons |
react-native-safe-area-context |
Handle iPhone notches | Prevent UI from hiding behind notch |
react-native-toast-message |
Show success messages | User feedback when saving |
Step 4: Configure Your Project
4.1 Update app.json
Open app.json and replace its contents with:
{
"expo": {
"name": "Address Picker",
"slug": "address-picker-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourname.addresspicker",
"infoPlist": {
"NSLocationWhenInUseUsageDescription": "We need your location to help you add addresses accurately.",
"NSLocationAlwaysAndWhenInUseUsageDescription": "We need your location to help you add addresses accurately."
},
"config": {
"googleMapsApiKey": "YOUR_API_KEY_HERE"
}
},
"android": {
"package": "com.yourname.addresspicker",
"permissions": [
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION"
],
"config": {
"googleMaps": {
"apiKey": "YOUR_API_KEY_HERE"
}
}
},
"plugins": [
[
"react-native-maps",
{
"iosGoogleMapsApiKey": "YOUR_API_KEY_HERE",
"androidGoogleMapsApiKey": "YOUR_API_KEY_HERE"
}
]
]
}
}
π΄ IMPORTANT: Replace YOUR_API_KEY_HERE (appears 4 times!) with your actual API key from Step 1.
What's happening here?
-
infoPlist: iOS permission messages users will see -
permissions: Android permissions we need -
plugins: Tells Expo to configure react-native-maps -
googleMapsApiKey: Your API key for the map to work
4.2 Rebuild the Project
Since we added native dependencies, rebuild:
npx expo prebuild
# Then start again
npx expo run:ios
# or
npx expo run:android
What's happening?
-
prebuild: Generates native iOS/Android code with our dependencies - You'll see new
ios/andandroid/folders created
β
Checkpoint: Run npx expo start - it should start without errors.
π» Part 2: Building the Features
Now the fun part! We'll build this step by step.
Step 5: Create the Basic Map Screen
5.1 Create the File
Create a new file: app/AddAddressScreen.js
import React, { useState, useRef } from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import MapView, { PROVIDER_GOOGLE } from 'react-native-maps';
const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window');
export default function AddAddressScreen() {
// State to track the map's current region (visible area)
const [mapRegion, setMapRegion] = useState({
latitude: 28.6139, // Default: Delhi, India
longitude: 77.2090,
latitudeDelta: 0.0922, // Zoom level (smaller = more zoomed in)
longitudeDelta: 0.0421,
});
// Reference to access map methods
const mapRef = useRef(null);
return (
<View style={styles.container}>
<MapView
ref={mapRef}
style={styles.map}
initialRegion={mapRegion}
provider={PROVIDER_GOOGLE}
showsUserLocation={true}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8F9FA',
},
map: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
},
});
What's happening here?
| Code | Purpose |
|---|---|
useState |
Creates a variable that triggers re-render when changed |
mapRegion |
Stores what part of the world the map shows |
latitude/longitude |
Center point of the map (Delhi coordinates) |
latitudeDelta |
How much latitude to show (controls zoom) |
useRef |
Creates a reference to call map methods later |
PROVIDER_GOOGLE |
Use Google Maps (not Apple Maps) |
showsUserLocation |
Shows blue dot for user's location |
5.2 Update App.js to Show Your Screen
Replace App.js with:
import React from 'react';
import AddAddressScreen from './app/AddAddressScreen';
export default function App() {
return <AddAddressScreen />;
}
5.3 Test It!
npx expo run:ios
npx expo run:android
Expected Result: You should see a full-screen Google Map centered on Delhi.
β Common Mistakes:
| Problem | Solution |
|---|---|
| Blank white screen | Check API key is correct in app.json
|
| "Google Maps not available" | Run npx expo prebuild again |
| Map shows but is gray | Enable billing in Google Cloud Console |
| "Invalid API key" | Make sure all 4 APIs are enabled in Step 1.2 |
Step 6: Add the Center Marker
Let's add a fixed marker in the center that stays put while the map moves underneath.
6.1 Update AddAddressScreen.js
Add this import at the top:
import Svg, { Path, Circle } from 'react-native-svg';
Add this component right after </MapView>:
{/* Center Marker - stays fixed while map moves */}
<View style={styles.centerMarker}>
<Svg width={36} height={36} viewBox="0 0 24 24" fill="none">
<Path
d="M21 10C21 17 12 23 12 23S3 17 3 10A9 9 0 0 1 12 1A9 9 0 0 1 21 10Z"
fill="#FF0000"
stroke="white"
strokeWidth="2"
/>
<Circle cx="12" cy="10" r="3" fill="white" />
</Svg>
</View>
Add these styles:
centerMarker: {
position: 'absolute',
top: '50%', // Center vertically
left: '50%', // Center horizontally
marginTop: -36, // Offset by half the marker height
marginLeft: -18, // Offset by half the marker width
zIndex: 1, // Show above map
pointerEvents: 'none', // Allow touches to pass through to map
},
What's happening here?
- position: 'absolute': Takes the marker out of normal flow, lets us position it precisely
- top: '50%', left: '50%': Center point of screen
- marginTop/marginLeft: Adjust so the marker's point (not top-left) is at center
- pointerEvents: 'none': Marker won't block map touches
- Svg: Custom vector graphic for a nice-looking pin
Why not use MapView.Marker?
- MapView.Marker moves with the map
- We want a fixed overlay that stays centered
- Better performance (no re-rendering when map moves)
6.2 Test It!
Expected Result: You should see a red pin marker fixed in the center. When you drag the map, the marker stays put!
Step 7: Add Current Location Button
Let's add a button that centers the map on the user's location.
7.1 Request Location Permission
Add these imports:
import { Alert, TouchableOpacity, ActivityIndicator } from 'react-native';
import * as Location from 'expo-location';
Add these state variables after your existing useState:
const [locationPermission, setLocationPermission] = useState(false);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [selectedLocation, setSelectedLocation] = useState(null);
Add this function (put it before the return statement):
// Request permission to access device location
const requestLocationPermission = async () => {
try {
// Ask user for permission
const { status } = await Location.requestForegroundPermissionsAsync();
// Store whether permission was granted
setLocationPermission(status === 'granted');
// If granted, get their location immediately
if (status === 'granted') {
getCurrentLocation();
} else {
Alert.alert(
'Permission Denied',
'We need location access to show your current position.'
);
}
} catch (error) {
console.error('Error requesting location permission:', error);
Alert.alert('Error', 'Unable to access location services.');
}
};
What's happening here?
| Code | Purpose |
|---|---|
requestForegroundPermissionsAsync() |
Shows iOS/Android permission popup |
status === 'granted' |
User tapped "Allow" |
Alert.alert() |
Shows a popup message to the user |
try/catch |
Handles errors gracefully |
7.2 Get Current Location
Add this function:
// Get user's current GPS coordinates
const getCurrentLocation = async () => {
setIsGettingLocation(true); // Show loading indicator
try {
// Get GPS coordinates
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High, // Use GPS, not just network
});
// Extract latitude and longitude
const { latitude, longitude } = location.coords;
// Create a new map region centered on user
const newRegion = {
latitude,
longitude,
latitudeDelta: 0.01, // Zoom in closer
longitudeDelta: 0.01,
};
// Animate the map to this location
if (mapRef.current) {
mapRef.current.animateToRegion(newRegion, 1000); // 1000ms animation
}
// Save the selected location
setSelectedLocation({ latitude, longitude });
} catch (error) {
console.error('Error getting current location:', error);
Alert.alert('Error', 'Unable to get your current location. Please try again.');
} finally {
setIsGettingLocation(false); // Hide loading indicator
}
};
What's happening here?
- accuracy: Location.Accuracy.High: Uses GPS satellite for precise location
- animateToRegion: Smoothly moves map to new location (not instant jump)
- 1000: Animation duration in milliseconds (1 second)
- finally: Runs whether success or error (perfect for hiding loaders)
7.3 Add the Button UI
Add this import:
import { Line } from 'react-native-svg';
Add this button after the center marker (before the closing </View>):
{/* Current Location Button */}
<TouchableOpacity
style={styles.currentLocationButton}
onPress={getCurrentLocation}
disabled={isGettingLocation}
>
{isGettingLocation ? (
<ActivityIndicator color="#FF0000" />
) : (
<Svg width={24} height={24} viewBox="0 0 24 24" fill="none">
<Line x1="2" x2="5" y1="12" y2="12" stroke="#FF0000" strokeWidth="2" />
<Line x1="19" x2="22" y1="12" y2="12" stroke="#FF0000" strokeWidth="2" />
<Line x1="12" x2="12" y1="2" y2="5" stroke="#FF0000" strokeWidth="2" />
<Line x1="12" x2="12" y1="19" y2="22" stroke="#FF0000" strokeWidth="2" />
<Circle cx="12" cy="12" r="7" stroke="#FF0000" strokeWidth="2" />
<Circle cx="12" cy="12" r="3" stroke="#FF0000" strokeWidth="2" />
</Svg>
)}
</TouchableOpacity>
Add these styles:
currentLocationButton: {
position: 'absolute',
right: 16,
top: 100,
backgroundColor: 'white',
width: 48,
height: 48,
borderRadius: 24,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5, // Android shadow
},
7.4 Call Permission on Mount
Add this useEffect after your functions:
import { useEffect } from 'react'; // Add to imports
// Run once when component loads
useEffect(() => {
requestLocationPermission();
}, []); // Empty array = run once on mount
What's happening here?
-
useEffect: Runs code at specific times -
[]: Empty dependency array means "run once when component first loads" - Automatically asks for permission when user opens the screen
7.5 Test It!
Expected Result:
- App asks for location permission
- You see a white circular button in the top-right
- Tap it β map animates to your current location
- While loading, button shows a spinner
β Common Mistakes:
| Problem | Solution |
|---|---|
| Permission not requested | Check useEffect is called correctly |
| "Location services disabled" | Enable GPS in phone settings |
| Button doesn't show | Check styles.currentLocationButton is defined |
| Crashes on button press | Wrap in try/catch as shown |
Step 8: Add Search with Autocomplete
Now let's add a search bar with Google Places suggestions.
8.1 Add Search State
Add these state variables:
const [searchQuery, setSearchQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [isSearching, setIsSearching] = useState(false);
8.2 Create Search Function
// Search for places using Google Places API
const searchPlaces = async (query) => {
// Don't search if query is too short
if (!query.trim() || query.length < 3) {
setSuggestions([]);
setShowSuggestions(false);
return;
}
try {
// Replace with your actual API key
const API_KEY = 'YOUR_API_KEY_HERE';
// Build the API URL
const url = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(query)}&key=${API_KEY}&components=country:in`;
// Make the request
const response = await fetch(url);
const data = await response.json();
// Check if we got results
if (data.status === 'OK' && data.predictions) {
// Show up to 5 suggestions
setSuggestions(data.predictions.slice(0, 5));
setShowSuggestions(true);
} else if (data.status === 'ZERO_RESULTS') {
setSuggestions([]);
setShowSuggestions(false);
} else {
console.error('Places API error:', data.status);
}
} catch (error) {
console.error('Search error:', error);
}
};
What's happening here?
| Code | Purpose |
|---|---|
query.trim() |
Remove spaces from start/end |
query.length < 3 |
Wait for 3+ characters before searching |
encodeURIComponent() |
Make query URL-safe (handles spaces, special chars) |
components=country:in |
Limit results to India (change to your country) |
slice(0, 5) |
Show only first 5 results |
8.3 Add Debouncing
Debouncing prevents API calls on every keystroke:
import { useCallback } from 'react'; // Add to imports
// Debounced search - waits 300ms after user stops typing
const debouncedSearch = useCallback(
(() => {
let timeoutId;
return (query) => {
// Cancel previous timeout
clearTimeout(timeoutId);
// Set new timeout
timeoutId = setTimeout(() => {
searchPlaces(query);
}, 300); // Wait 300ms
};
})(),
[] // No dependencies - function stays the same
);
// Handle search input change
const handleSearchChange = (text) => {
setSearchQuery(text);
debouncedSearch(text); // Call debounced version
};
What's debouncing?
Without debouncing:
- User types "pizza" β 5 API calls (p, pi, piz, pizz, pizza)
- Expensive! Slow! Hits rate limits!
With debouncing:
- User types "pizza" β 1 API call (after they stop typing)
- Waits 300ms after last keystroke
- Much better!
8.4 Get Place Details
When user taps a suggestion, we need to get its coordinates:
// Get coordinates for a selected place
const selectPlace = async (place) => {
setSearchQuery(place.description);
setShowSuggestions(false);
setIsSearching(true);
try {
const API_KEY = 'YOUR_API_KEY_HERE';
// Get detailed info about the place
const url = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${place.place_id}&fields=geometry&key=${API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.result?.geometry?.location) {
const { lat, lng } = data.result.geometry.location;
// Move map to this location
const newRegion = {
latitude: lat,
longitude: lng,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
};
if (mapRef.current) {
mapRef.current.animateToRegion(newRegion, 1000);
}
setSelectedLocation({ latitude: lat, longitude: lng });
}
} catch (error) {
console.error('Place details error:', error);
Alert.alert('Error', 'Unable to get location details.');
} finally {
setIsSearching(false);
}
};
8.5 Add Search UI
Add these imports:
import { TextInput, ScrollView, SafeAreaView } from 'react-native';
Add this code right after the opening <View style={styles.container}>:
{/* Search Bar */}
<SafeAreaView style={styles.searchContainer}>
<View style={styles.searchBar}>
<TextInput
style={styles.searchInput}
placeholder="Search for a place..."
value={searchQuery}
onChangeText={handleSearchChange}
returnKeyType="search"
/>
{searchQuery.length > 0 && (
<TouchableOpacity
onPress={() => {
setSearchQuery('');
setSuggestions([]);
setShowSuggestions(false);
}}
style={styles.clearButton}
>
<Text style={styles.clearButtonText}>β</Text>
</TouchableOpacity>
)}
</View>
{/* Suggestions Dropdown */}
{showSuggestions && suggestions.length > 0 && (
<View style={styles.suggestionsContainer}>
<ScrollView style={styles.suggestionsList}>
{suggestions.map((place) => (
<TouchableOpacity
key={place.place_id}
style={styles.suggestionItem}
onPress={() => selectPlace(place)}
>
<Text style={styles.suggestionText}>π {place.description}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
</SafeAreaView>
Add these styles:
searchContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 10,
padding: 16,
},
searchBar: {
flexDirection: 'row',
backgroundColor: 'white',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
alignItems: 'center',
},
searchInput: {
flex: 1,
fontSize: 16,
color: '#333',
},
clearButton: {
padding: 4,
},
clearButtonText: {
fontSize: 18,
color: '#999',
},
suggestionsContainer: {
backgroundColor: 'white',
borderRadius: 12,
marginTop: 8,
maxHeight: 200,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
suggestionsList: {
maxHeight: 200,
},
suggestionItem: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
suggestionText: {
fontSize: 14,
color: '#333',
},
8.6 Test It!
Expected Result:
- Type "coffee" in the search bar
- After 300ms, see suggestions appear
- Tap a suggestion β map moves to that location
- See the β button to clear search
Step 9: Add Reverse Geocoding
Reverse geocoding = converting coordinates to an address.
9.1 Add State for Address
const [displayAddress, setDisplayAddress] = useState('');
const [isFetchingAddress, setIsFetchingAddress] = useState(false);
const [addressDetails, setAddressDetails] = useState({
streetNumber: '',
route: '',
area: '',
city: '',
pincode: '',
state: '',
});
9.2 Create Reverse Geocoding Function
// Convert coordinates to address
const getAddressFromCoordinates = async (latitude, longitude) => {
setIsFetchingAddress(true);
try {
const API_KEY = 'YOUR_API_KEY_HERE';
const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${API_KEY}`;
const response = await fetch(url);
const data = await response.json();
if (data.status === 'OK' && data.results && data.results.length > 0) {
const result = data.results[0];
// Get the formatted address
setDisplayAddress(result.formatted_address);
// Extract detailed components
const components = result.address_components;
let details = {
streetNumber: '',
route: '',
area: '',
city: '',
pincode: '',
state: '',
};
components.forEach((component) => {
const types = component.types;
if (types.includes('street_number')) {
details.streetNumber = component.long_name;
}
if (types.includes('route')) {
details.route = component.long_name;
}
if (types.includes('sublocality_level_1') || types.includes('sublocality')) {
details.area = component.long_name;
}
if (types.includes('locality')) {
details.city = component.long_name;
}
if (types.includes('postal_code')) {
details.pincode = component.long_name;
}
if (types.includes('administrative_area_level_1')) {
details.state = component.long_name;
}
});
setAddressDetails(details);
console.log('Address found:', result.formatted_address);
console.log('Details:', details);
}
} catch (error) {
console.error('Reverse geocoding error:', error);
} finally {
setIsFetchingAddress(false);
}
};
What's happening here?
Google returns address in components like:
{
"street_number": "123",
"route": "Main Street",
"locality": "Mumbai",
"postal_code": "400001"
}
We extract each piece and store it separately.
9.3 Trigger on Map Movement
Update your MapView component:
<MapView
ref={mapRef}
style={styles.map}
initialRegion={mapRegion}
provider={PROVIDER_GOOGLE}
showsUserLocation={locationPermission}
onRegionChangeComplete={(region) => {
setMapRegion(region);
setSelectedLocation({
latitude: region.latitude,
longitude: region.longitude,
});
// Get address when map stops moving
getAddressFromCoordinates(region.latitude, region.longitude);
}}
/>
What's happening?
-
onRegionChangeComplete: Fires when user stops dragging the map - Automatically gets the address for the center location
- Real-time address updates as you move the map!
9.4 Add Address Display
Add this component above the current location button:
{/* Address Display */}
{displayAddress ? (
<View style={styles.addressCard}>
<Text style={styles.addressLabel}>Selected Location:</Text>
<Text style={styles.addressText}>{displayAddress}</Text>
{isFetchingAddress && (
<ActivityIndicator size="small" color="#FF0000" style={{ marginTop: 8 }} />
)}
</View>
) : null}
Add these styles:
addressCard: {
position: 'absolute',
bottom: 100,
left: 16,
right: 16,
backgroundColor: 'white',
padding: 16,
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
},
addressLabel: {
fontSize: 12,
color: '#666',
marginBottom: 4,
},
addressText: {
fontSize: 14,
color: '#333',
fontWeight: '500',
},
9.5 Test It!
Expected Result:
- Drag the map around
- When you stop, see the address update
- Shows a loading indicator while fetching
- Address appears in a white card at the bottom
Step 10: Add Address Form
Now let's create a form to save the address.
10.1 Add Form State
const [formData, setFormData] = useState({
houseNo: '',
area: '',
pincode: '',
city: '',
state: '',
addressType: 'Home', // Home, Work, or Other
});
const [formErrors, setFormErrors] = useState({});
10.2 Create Bottom Sheet
Install one more package:
npm install react-native-reanimated
Add this simpler approach - a fixed bottom form:
{/* Address Form */}
<View style={styles.formContainer}>
<ScrollView style={styles.formScroll}>
<Text style={styles.formTitle}>Address Details</Text>
{/* House Number */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>House/Flat/Floor No. *</Text>
<TextInput
style={[styles.input, formErrors.houseNo && styles.inputError]}
placeholder="Enter house number"
value={formData.houseNo}
onChangeText={(text) => setFormData({ ...formData, houseNo: text })}
/>
{formErrors.houseNo && (
<Text style={styles.errorText}>{formErrors.houseNo}</Text>
)}
</View>
{/* Area */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Road/Area *</Text>
<TextInput
style={[styles.input, formErrors.area && styles.inputError]}
placeholder="Enter area"
value={formData.area}
onChangeText={(text) => setFormData({ ...formData, area: text })}
/>
{formErrors.area && (
<Text style={styles.errorText}>{formErrors.area}</Text>
)}
</View>
{/* Pincode */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Pincode *</Text>
<TextInput
style={[styles.input, formErrors.pincode && styles.inputError]}
placeholder="Enter pincode"
value={formData.pincode}
onChangeText={(text) => setFormData({ ...formData, pincode: text })}
keyboardType="numeric"
maxLength={6}
/>
{formErrors.pincode && (
<Text style={styles.errorText}>{formErrors.pincode}</Text>
)}
</View>
{/* City */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>City *</Text>
<TextInput
style={[styles.input, formErrors.city && styles.inputError]}
placeholder="Enter city"
value={formData.city}
onChangeText={(text) => setFormData({ ...formData, city: text })}
/>
{formErrors.city && (
<Text style={styles.errorText}>{formErrors.city}</Text>
)}
</View>
{/* State */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>State *</Text>
<TextInput
style={[styles.input, formErrors.state && styles.inputError]}
placeholder="Enter state"
value={formData.state}
onChangeText={(text) => setFormData({ ...formData, state: text })}
/>
{formErrors.state && (
<Text style={styles.errorText}>{formErrors.state}</Text>
)}
</View>
{/* Address Type */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>Save as</Text>
<View style={styles.typeButtons}>
{['Home', 'Work', 'Other'].map((type) => (
<TouchableOpacity
key={type}
style={[
styles.typeButton,
formData.addressType === type && styles.typeButtonActive,
]}
onPress={() => setFormData({ ...formData, addressType: type })}
>
<Text
style={[
styles.typeButtonText,
formData.addressType === type && styles.typeButtonTextActive,
]}
>
{type}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Save Button */}
<TouchableOpacity
style={styles.saveButton}
onPress={handleSaveAddress}
>
<Text style={styles.saveButtonText}>Save Address</Text>
</TouchableOpacity>
</ScrollView>
</View>
Add these styles:
formContainer: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: SCREEN_HEIGHT * 0.6,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 10,
},
formScroll: {
padding: 16,
},
formTitle: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 16,
color: '#333',
},
inputGroup: {
marginBottom: 16,
},
inputLabel: {
fontSize: 14,
color: '#666',
marginBottom: 6,
},
input: {
backgroundColor: '#F8F9FA',
borderRadius: 8,
paddingHorizontal: 16,
paddingVertical: 12,
fontSize: 16,
borderWidth: 1,
borderColor: '#E0E0E0',
},
inputError: {
borderColor: '#FF0000',
},
errorText: {
color: '#FF0000',
fontSize: 12,
marginTop: 4,
},
typeButtons: {
flexDirection: 'row',
gap: 8,
},
typeButton: {
flex: 1,
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
backgroundColor: 'white',
alignItems: 'center',
},
typeButtonActive: {
borderColor: '#FF0000',
backgroundColor: '#FFF5F5',
},
typeButtonText: {
fontSize: 14,
color: '#666',
},
typeButtonTextActive: {
color: '#FF0000',
fontWeight: '600',
},
saveButton: {
backgroundColor: '#FF0000',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
marginTop: 8,
marginBottom: 32,
},
saveButtonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
10.3 Auto-fill Form from Address
Update getAddressFromCoordinates to auto-fill the form:
// After setting addressDetails, add:
setFormData({
houseNo: details.streetNumber || formData.houseNo,
area: details.area || details.route || formData.area,
pincode: details.pincode || formData.pincode,
city: details.city || formData.city,
state: details.state || formData.state,
addressType: formData.addressType, // Keep selected type
});
10.4 Add Validation and Save
// Validate form fields
const validateForm = () => {
const errors = {};
if (!formData.houseNo.trim()) {
errors.houseNo = 'House number is required';
}
if (!formData.area.trim()) {
errors.area = 'Area is required';
}
if (!formData.pincode.trim()) {
errors.pincode = 'Pincode is required';
} else if (formData.pincode.length !== 6) {
errors.pincode = 'Pincode must be 6 digits';
}
if (!formData.city.trim()) {
errors.city = 'City is required';
}
if (!formData.state.trim()) {
errors.state = 'State is required';
}
if (!selectedLocation) {
errors.location = 'Please select a location on the map';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
// Save the address
const handleSaveAddress = () => {
if (!validateForm()) {
Alert.alert('Validation Error', 'Please fill in all required fields');
return;
}
// Here you would typically save to your backend
const addressData = {
...formData,
latitude: selectedLocation.latitude,
longitude: selectedLocation.longitude,
formattedAddress: displayAddress,
};
console.log('Saving address:', addressData);
// Show success message
Alert.alert(
'Success!',
'Address saved successfully',
[
{
text: 'OK',
onPress: () => {
// Reset form or navigate back
console.log('Address saved!');
},
},
]
);
};
10.5 Test It!
Expected Result:
- See a form at the bottom with address fields
- Move the map β fields auto-fill
- Tap "Save Address" β validation checks
- If valid β shows success alert
- If invalid β shows error messages in red
π Troubleshooting Guide
Map Issues
Problem: Blank white screen
-
Solution:
- Check API key is correct in
app.json - Verify billing is enabled in Google Cloud Console
- Make sure all 4 APIs are enabled
- Run
npx expo prebuildagain
- Check API key is correct in
Problem: Map is gray/blank tiles
- Solution: Enable billing in Google Cloud Console
Problem: "Google Maps not available"
-
Solution: Run
npx expo prebuild --cleanthennpx expo start
Location Issues
Problem: Permission modal doesn't appear
-
Solution: Check
app.jsonhasNSLocationWhenInUseUsageDescription(iOS) andACCESS_FINE_LOCATION(Android)
Problem: Current location button doesn't work
-
Solution:
- Enable Location Services in device settings
- Grant app location permission
- Test on a real device (emulators have issues)
Problem: "Location timeout"
-
Solution:
- Make sure you're testing outdoors or near a window
- Increase timeout:
Location.getCurrentPositionAsync({ timeout: 30000 })
Search Issues
Problem: No search suggestions appear
-
Solution:
- Check API key in
searchPlacesfunction - Verify Places API is enabled in Google Cloud Console
- Check console for error messages
- Check API key in
Problem: "This API project is not authorized"
-
Solution:
- Go to Google Cloud Console
- APIs & Services β Credentials
- Edit your API key
- Remove all restrictions temporarily to test
- Re-add restrictions once working
Problem: Search works but can't select location
-
Solution: Check the
selectPlacefunction has correct API key
Reverse Geocoding Issues
Problem: Address not showing
-
Solution:
- Check API key in
getAddressFromCoordinates - Verify Geocoding API is enabled
- Check console.log for error messages
- Check API key in
Problem: Address shows but doesn't update
-
Solution: Make sure
onRegionChangeCompleteis calling the function
Form Issues
Problem: Form doesn't auto-fill
-
Solution: Check
setFormDatais called ingetAddressFromCoordinates
Problem: Validation not working
-
Solution: Verify
validateForm()is called beforehandleSaveAddress
Problem: Keyboard covers inputs
-
Solution: Wrap form in
KeyboardAvoidingView:
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
Top comments (0)