Package Exports
- @trackthatride/react-native-map
Readme
@trackthatride/react-native-map
React Native SDK for building mobile delivery tracking applications. Includes components and hooks optimized for iOS and Android with background mode support, battery optimization, and native map integration.
Installation
npm install @trackthatride/react-native-mapRequired Dependencies
npm install react-native-mapsiOS Setup
- Install CocoaPods dependencies:
cd ios && pod install && cd ..- Add location permissions to
ios/YourApp/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show delivery tracking</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need your location to provide real-time tracking updates</string>- Enable Google Maps (optional, for Google Maps instead of Apple Maps):
Add to ios/Podfile:
# Uncomment the next line to use Google Maps
# pod 'GoogleMaps'Android Setup
- Add Google Maps API key to
android/app/src/main/AndroidManifest.xml:
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
</application>- Add permissions to
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />- Update
android/build.gradle:
buildscript {
ext {
googlePlayServicesVersion = "18.0.0"
androidMapsUtilsVersion = "2.3.0"
}
}Quick Start
import { TrackingMap, TrackingInfo } from '@trackthatride/react-native-map';
import { View } from 'react-native';
function TrackingScreen() {
const [trackingState, setTrackingState] = useState(null);
return (
<View style={{ flex: 1 }}>
<TrackingMap
trackingCode="TR123456789"
apiBaseUrl="https://your-app.replit.app"
onStateChange={setTrackingState}
style={{ flex: 1 }}
/>
{trackingState && (
<TrackingInfo
state={trackingState}
showDriverDetails
showETA
/>
)}
</View>
);
}Components
TrackingMap
Native map component with real-time tracking, optimized for mobile devices.
Props
interface TrackingMapProps {
trackingCode: string; // Required: Tracking code to display
apiBaseUrl?: string; // Optional: API base URL
style?: ViewStyle; // Optional: Container style
initialRegion?: Region; // Optional: Initial map region
mapType?: 'standard' | 'satellite' | 'hybrid' | 'terrain';
enableRealtime?: boolean; // Optional: Enable SSE (default: true)
showRoute?: boolean; // Optional: Show route (default: true)
showsUserLocation?: boolean; // Optional: Show user location (default: false)
followsUserLocation?: boolean; // Optional: Follow user (default: false)
markers?: { // Optional: Custom markers
pickup?: { image?: any; title?: string; description?: string };
delivery?: { image?: any; title?: string; description?: string };
driver?: { image?: any; title?: string; description?: string };
};
onStateChange?: (state) => void; // Optional: State change callback
onMapReady?: () => void; // Optional: Map ready callback
onError?: (error: Error) => void; // Optional: Error callback
onMarkerPress?: (markerId, coord) => void; // Optional: Marker press callback
mapViewProps?: Partial<MapViewProps>; // Optional: Additional MapView props
}Basic Usage
import { TrackingMap } from '@trackthatride/react-native-map';
<TrackingMap
trackingCode="TR123456789"
apiBaseUrl="https://your-app.replit.app"
style={{ flex: 1 }}
/>Custom Region
<TrackingMap
trackingCode="TR123456789"
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
mapType="hybrid"
/>Custom Markers
<TrackingMap
trackingCode="TR123456789"
markers={{
pickup: {
image: require('./assets/pickup-marker.png'),
title: 'Pickup Location',
},
delivery: {
image: require('./assets/delivery-marker.png'),
title: 'Delivery Location',
},
driver: {
image: require('./assets/car-marker.png'),
title: 'Driver',
},
}}
/>Event Handling
<TrackingMap
trackingCode="TR123456789"
onStateChange={(state) => {
console.log('Ride status:', state.ride?.status);
}}
onMarkerPress={(markerId, coordinate) => {
console.log(`Marker ${markerId} pressed at:`, coordinate);
}}
onError={(error) => {
Alert.alert('Error', error.message);
}}
onMapReady={() => {
console.log('Map is ready');
}}
/>TrackingInfo
Display formatted tracking information with native styling.
Props
interface TrackingInfoProps {
state: TrackingSessionState; // Required: Current tracking state
style?: ViewStyle; // Optional: Container style
showDriverDetails?: boolean; // Optional: Show driver info (default: true)
showETA?: boolean; // Optional: Show ETA (default: true)
showConnectionStatus?: boolean; // Optional: Show status (default: false)
colors?: { // Optional: Color scheme
background?: string;
text?: string;
accent?: string;
success?: string;
warning?: string;
error?: string;
};
formatters?: { // Optional: Custom formatters
time?: (timestamp: string) => string;
status?: (status: string) => string;
eta?: (eta: string) => string;
};
textStyles?: { // Optional: Text styles
title?: TextStyle;
subtitle?: TextStyle;
body?: TextStyle;
caption?: TextStyle;
};
}Basic Usage
<TrackingInfo
state={trackingState}
showDriverDetails
showETA
showConnectionStatus
/>Custom Colors
<TrackingInfo
state={trackingState}
colors={{
background: '#FFFFFF',
text: '#000000',
accent: '#007AFF',
success: '#34C759',
warning: '#FF9500',
error: '#FF3B30',
}}
/>Custom Formatting
<TrackingInfo
state={trackingState}
formatters={{
time: (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
});
},
eta: (eta) => {
const minutes = Math.ceil(
(new Date(eta) - new Date()) / 60000
);
return minutes < 60
? `${minutes} min`
: `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
},
}}
/>Hooks
useTrackingSession
React Native hook with mobile-specific optimizations including background mode support.
API
function useTrackingSession(config?: TrackingSessionConfig): {
state: TrackingSessionState | null;
isConnected: boolean;
connectionStatus: ConnectionStatus;
connect: (trackingCode: string) => Promise<void>;
disconnect: () => void;
refresh: () => Promise<void>;
error: Error | null;
isBackground: boolean; // Mobile-specific: background state
}Basic Usage
import { useTrackingSession } from '@trackthatride/react-native-map';
import { useEffect } from 'react';
function TrackingScreen() {
const {
state,
isConnected,
connect,
disconnect,
error,
isBackground,
} = useTrackingSession({
baseUrl: 'https://your-app.replit.app',
});
useEffect(() => {
connect('TR123456789');
return () => disconnect();
}, []);
if (error) {
return <Text>Error: {error.message}</Text>;
}
return (
<View>
<Text>Status: {state?.ride?.status}</Text>
<Text>Background: {isBackground ? 'Yes' : 'No'}</Text>
</View>
);
}Background Mode Handling
The hook automatically handles background mode:
function TrackingScreen() {
const { state, isBackground } = useTrackingSession({
baseUrl: 'https://your-app.replit.app',
});
useEffect(() => {
connect('TR123456789');
}, []);
// Hook automatically:
// - Disconnects SSE when app goes to background
// - Switches to 30-second polling in background
// - Reconnects SSE when app comes to foreground
return (
<View>
{isBackground && (
<Text>Running in background mode (polling every 30s)</Text>
)}
<Text>Status: {state?.ride?.status}</Text>
</View>
);
}Complete Examples
Full Tracking Screen
import React, { useState, useEffect } from 'react';
import {
View,
StyleSheet,
SafeAreaView,
StatusBar,
Alert,
} from 'react-native';
import {
TrackingMap,
TrackingInfo,
} from '@trackthatride/react-native-map';
function FullTrackingScreen({ route }) {
const { trackingCode } = route.params;
const [trackingState, setTrackingState] = useState(null);
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<View style={styles.mapContainer}>
<TrackingMap
trackingCode={trackingCode}
apiBaseUrl="https://your-app.replit.app"
onStateChange={setTrackingState}
onError={(error) => {
Alert.alert('Error', error.message);
}}
style={styles.map}
enableRealtime
showRoute
/>
</View>
<View style={styles.infoContainer}>
{trackingState ? (
<TrackingInfo
state={trackingState}
showDriverDetails
showETA
showConnectionStatus
style={styles.info}
/>
) : (
<Text style={styles.loading}>Loading tracking information...</Text>
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
},
mapContainer: {
flex: 2,
},
map: {
flex: 1,
},
infoContainer: {
flex: 1,
},
info: {
flex: 1,
},
loading: {
padding: 20,
textAlign: 'center',
color: '#666',
},
});
export default FullTrackingScreen;Custom Tracking Dashboard
import React, { useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
} from 'react-native';
import { useTrackingSession } from '@trackthatride/react-native-map';
function TrackingDashboard({ trackingCode }) {
const {
state,
connect,
disconnect,
refresh,
error,
connectionStatus,
isBackground,
} = useTrackingSession({
baseUrl: 'https://your-app.replit.app',
});
useEffect(() => {
connect(trackingCode);
return () => disconnect();
}, [trackingCode]);
const handleRefresh = async () => {
try {
await refresh();
} catch (err) {
Alert.alert('Refresh Failed', err.message);
}
};
if (error) {
return (
<View style={styles.error}>
<Text style={styles.errorText}>Error: {error.message}</Text>
<TouchableOpacity onPress={handleRefresh} style={styles.button}>
<Text style={styles.buttonText}>Retry</Text>
</TouchableOpacity>
</View>
);
}
if (!state?.ride) {
return (
<View style={styles.loading}>
<Text>Loading...</Text>
</View>
);
}
const { ride, driver, estimatedArrival } = state;
return (
<ScrollView style={styles.container}>
{/* Connection Status */}
<View style={styles.statusBar}>
<View style={[
styles.statusDot,
{ backgroundColor: getStatusColor(connectionStatus) }
]} />
<Text style={styles.statusText}>
{connectionStatus} {isBackground && '(background)'}
</Text>
</View>
{/* Tracking Code */}
<View style={styles.card}>
<Text style={styles.title}>{ride.tracking_code}</Text>
<View style={[
styles.badge,
{ backgroundColor: getRideStatusColor(ride.status) }
]}>
<Text style={styles.badgeText}>{ride.status.toUpperCase()}</Text>
</View>
</View>
{/* Customer Info */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Customer</Text>
<Text style={styles.cardText}>{ride.customer_name}</Text>
<Text style={styles.cardSubtext}>{ride.phone_number}</Text>
</View>
{/* Addresses */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Pickup</Text>
<Text style={styles.cardText}>{ride.pickup_address}</Text>
<Text style={[styles.cardTitle, styles.marginTop]}>Delivery</Text>
<Text style={styles.cardText}>{ride.delivery_address}</Text>
</View>
{/* Driver Info */}
{driver && (
<View style={styles.card}>
<Text style={styles.cardTitle}>Driver</Text>
<Text style={styles.cardText}>
{driver.firstName} {driver.lastName}
</Text>
{driver.mobileNumber && (
<Text style={styles.cardSubtext}>{driver.mobileNumber}</Text>
)}
{driver.vehicleType && driver.vehiclePlate && (
<Text style={styles.cardSubtext}>
{driver.vehicleType} • {driver.vehiclePlate}
</Text>
)}
</View>
)}
{/* ETA */}
{estimatedArrival && (
<View style={styles.card}>
<Text style={styles.cardTitle}>Estimated Arrival</Text>
<Text style={styles.etaText}>
{formatETA(estimatedArrival)}
</Text>
</View>
)}
{/* Refresh Button */}
<TouchableOpacity onPress={handleRefresh} style={styles.refreshButton}>
<Text style={styles.refreshButtonText}>Refresh</Text>
</TouchableOpacity>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
statusBar: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
statusDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 8,
},
statusText: {
fontSize: 12,
color: '#666',
},
card: {
backgroundColor: '#FFFFFF',
padding: 16,
marginTop: 12,
marginHorizontal: 12,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
badge: {
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
marginTop: 8,
},
badgeText: {
fontSize: 12,
fontWeight: '600',
color: '#FFFFFF',
},
cardTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 8,
},
cardText: {
fontSize: 16,
color: '#333',
},
cardSubtext: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
marginTop: {
marginTop: 16,
},
etaText: {
fontSize: 20,
fontWeight: '600',
color: '#007AFF',
},
refreshButton: {
backgroundColor: '#007AFF',
padding: 16,
margin: 12,
borderRadius: 8,
alignItems: 'center',
},
refreshButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
error: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: 16,
color: '#FF3B30',
marginBottom: 20,
textAlign: 'center',
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
const getStatusColor = (status) => {
const colors = {
connected: '#34C759',
connecting: '#FF9500',
reconnecting: '#FF9500',
disconnected: '#FF3B30',
};
return colors[status] || '#999';
};
const getRideStatusColor = (status) => {
const colors = {
pending: '#FF9500',
assigned: '#007AFF',
in_transit: '#34C759',
delivered: '#34C759',
cancelled: '#FF3B30',
};
return colors[status] || '#999';
};
const formatETA = (eta) => {
const minutes = Math.ceil((new Date(eta) - new Date()) / 60000);
if (minutes <= 0) return 'Arrived';
if (minutes < 60) return `${minutes} min`;
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
};
export default TrackingDashboard;Tracking with Navigation
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import TrackingListScreen from './screens/TrackingListScreen';
import TrackingDetailScreen from './screens/TrackingDetailScreen';
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="TrackingList"
component={TrackingListScreen}
options={{ title: 'My Deliveries' }}
/>
<Stack.Screen
name="TrackingDetail"
component={TrackingDetailScreen}
options={{ title: 'Delivery Tracking' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
// TrackingListScreen.tsx
function TrackingListScreen({ navigation }) {
const trackingCodes = ['TR123456789', 'TR987654321', 'TR555555555'];
return (
<ScrollView>
{trackingCodes.map(code => (
<TouchableOpacity
key={code}
style={styles.listItem}
onPress={() => navigation.navigate('TrackingDetail', { trackingCode: code })}
>
<Text style={styles.listItemText}>{code}</Text>
</TouchableOpacity>
))}
</ScrollView>
);
}
// TrackingDetailScreen.tsx
function TrackingDetailScreen({ route }) {
const { trackingCode } = route.params;
const [state, setState] = useState(null);
return (
<View style={{ flex: 1 }}>
<TrackingMap
trackingCode={trackingCode}
onStateChange={setState}
style={{ flex: 1 }}
/>
{state && <TrackingInfo state={state} />}
</View>
);
}Mobile-Specific Features
Background Mode Support
The SDK automatically optimizes for battery life when the app is in the background:
- Foreground: Real-time SSE updates
- Background: Polling every 30 seconds
- Automatic switching: Handles AppState changes
// Automatic background handling
const { isBackground } = useTrackingSession({
baseUrl: 'https://your-app.replit.app',
});
// Show UI indicator
{isBackground && (
<Text>Background Mode (reduced updates)</Text>
)}Location Permissions
Request location permissions for enhanced tracking:
import { PermissionsAndroid, Platform } from 'react-native';
import Geolocation from '@react-native-community/geolocation';
async function requestLocationPermission() {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
return true; // iOS permissions handled via Info.plist
}
// Use in component
useEffect(() => {
requestLocationPermission().then(granted => {
if (granted) {
setShowsUserLocation(true);
}
});
}, []);Deep Linking
Support deep links for tracking codes:
// App.tsx
import { Linking } from 'react-native';
useEffect(() => {
const handleDeepLink = ({ url }) => {
// trackme://track/TR123456789
const trackingCode = url.split('/').pop();
navigation.navigate('TrackingDetail', { trackingCode });
};
Linking.addEventListener('url', handleDeepLink);
// Handle app launched via deep link
Linking.getInitialURL().then(url => {
if (url) {
handleDeepLink({ url });
}
});
return () => {
Linking.removeEventListener('url', handleDeepLink);
};
}, []);Push Notifications
Integrate with push notifications for tracking updates:
import messaging from '@react-native-firebase/messaging';
useEffect(() => {
const unsubscribe = messaging().onMessage(async remoteMessage => {
if (remoteMessage.data?.trackingCode) {
// Update tracking display
await refresh();
}
});
return unsubscribe;
}, []);Performance Optimization
Memoization
import { useMemo, useCallback } from 'react';
function TrackingScreen() {
const handleStateChange = useCallback((state) => {
setTrackingState(state);
}, []);
const markers = useMemo(() => ({
pickup: {
image: require('./assets/pickup.png'),
title: 'Pickup',
},
delivery: {
image: require('./assets/delivery.png'),
title: 'Delivery',
},
}), []);
return (
<TrackingMap
trackingCode="TR123456789"
onStateChange={handleStateChange}
markers={markers}
/>
);
}Image Optimization
// Use optimized marker images
const markers = {
pickup: {
image: require('./assets/markers/pickup@2x.png'),
},
delivery: {
image: require('./assets/markers/delivery@2x.png'),
},
};Lazy Loading
import React, { lazy, Suspense } from 'react';
const TrackingMap = lazy(() => import('@trackthatride/react-native-map'));
function App() {
return (
<Suspense fallback={<ActivityIndicator />}>
<TrackingMap trackingCode="TR123456789" />
</Suspense>
);
}Best Practices
Handle Platform Differences: Use
Platform.OSfor platform-specific code.Request Permissions: Always request location permissions before using location features.
Implement Error Boundaries: Wrap tracking components in error boundaries.
Optimize for Battery: The hook handles this automatically, but be aware of background behavior.
Test on Real Devices: Always test location and map features on physical devices.
Handle Network Issues: Implement proper error handling for offline scenarios.
Use SafeAreaView: Always wrap in SafeAreaView for notch/island devices.
Troubleshooting
Map Not Displaying
iOS:
- Verify CocoaPods are installed
- Check Info.plist has location permissions
- For Google Maps, verify API key is configured
Android:
- Check Google Maps API key in AndroidManifest.xml
- Verify permissions are declared
- Ensure Google Play Services is available
Location Not Working
- Check permissions in device settings
- Verify Info.plist (iOS) / AndroidManifest.xml (Android) configuration
- Test on physical device (simulator location may be unreliable)
Background Updates Not Working
- Verify background modes are enabled (iOS)
- Check battery optimization settings (Android)
- Review logs for background task errors
Memory Issues
- Ensure
destroy()is called or hook cleanup runs - Avoid creating multiple sessions simultaneously
- Check for memory leaks with React DevTools Profiler
Platform-Specific Notes
iOS
- Requires location permissions in Info.plist
- Background location requires additional capabilities
- Simulator may not accurately reflect real device behavior
- MapKit is used by default (free), Google Maps is optional
Android
- Requires Google Maps API key
- Uses Google Maps by default
- Battery optimization may affect background updates
- Test on multiple Android versions (API 21+)
TypeScript Support
Full TypeScript support with all React Native types:
import {
TrackingMap,
TrackingInfo,
useTrackingSession,
TrackingMapProps,
TrackingInfoProps,
UseTrackingSessionResult,
TrackingSessionState,
} from '@trackthatride/react-native-map';
// All types from react-native-maps are also available
import type { Region, LatLng, MapViewProps } from 'react-native-maps';Testing
Unit Tests
import { renderHook } from '@testing-library/react-hooks';
import { useTrackingSession } from '@trackthatride/react-native-map';
describe('useTrackingSession', () => {
it('should connect to tracking code', async () => {
const { result } = renderHook(() =>
useTrackingSession({ baseUrl: 'https://test.com' })
);
await act(async () => {
await result.current.connect('TR123456789');
});
expect(result.current.isConnected).toBe(true);
});
});Integration Tests
import { render, waitFor } from '@testing-library/react-native';
import { TrackingMap } from '@trackthatride/react-native-map';
describe('TrackingMap', () => {
it('should render map and markers', async () => {
const { getByTestId } = render(
<TrackingMap
trackingCode="TR123456789"
apiBaseUrl="https://test.com"
/>
);
await waitFor(() => {
expect(getByTestId('tracking-map')).toBeTruthy();
});
});
});License
MIT
Support
For support, please contact Track That Ride support or visit our documentation at /docs on your Track That Ride instance.
Additional Resources
- React Native Maps Documentation
- React Native Location
- React Navigation
- React Native Firebase (for push notifications)