Documentation Index
Fetch the complete documentation index at: https://docs.bookovia.com/llms.txt
Use this file to discover all available pages before exploring further.
Bookovia React Native SDK
The official React Native SDK for the Bookovia Telematics API provides a TypeScript-first, cross-platform implementation optimized for React Native applications with React hooks, native modules, and background processing support.Features
- ✅ TypeScript First - Built with TypeScript for type safety and better DX
- ✅ React Hooks - Custom hooks for seamless state management
- ✅ Cross-Platform - Single codebase for iOS and Android
- ✅ Native Modules - Platform-specific optimizations and features
- ✅ Background Processing - Continuous location tracking and data sync
- ✅ Offline Support - Local storage with automatic sync
Installation
npm
npm install @bookovia/react-native-sdk
yarn
yarn add @bookovia/react-native-sdk
Install iOS Dependencies
cd ios && pod install
- React Native 0.70+
- iOS 13.0+
- Android API level 21+
Setup
Android Configuration
Add permissions toandroid/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Network permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Background services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<!-- Location tracking service -->
<service
android:name="com.bookovia.reactnative.LocationService"
android:exported="false" />
</application>
</manifest>
iOS Configuration
Add toios/YourApp/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to track vehicle trips</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs background location for continuous trip tracking</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
<string>background-fetch</string>
<string>background-processing</string>
</array>
SDK Initialization
import { BookoviaProvider, BookoviaConfig } from '@bookovia/react-native-sdk';
const config: BookoviaConfig = {
apiKey: 'bkv_live_your_api_key',
baseUrl: 'https://api.bookovia.com/v1',
enableOfflineMode: true,
enableDebugLogging: __DEV__,
};
export default function App() {
return (
<BookoviaProvider config={config}>
<YourAppContent />
</BookoviaProvider>
);
}
Quick Start
Basic Trip Management
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, Alert } from 'react-native';
import { useBookovia, useTripManager } from '@bookovia/react-native-sdk';
import type { Trip, StartTripRequest } from '@bookovia/react-native-sdk/types';
export const TripScreen: React.FC = () => {
const { client } = useBookovia();
const { currentTrip, isLoading, error, startTrip, stopTrip } = useTripManager();
const [vehicleId, setVehicleId] = useState('vehicle_123');
const handleStartTrip = async () => {
try {
const request: StartTripRequest = {
vehicleId,
driverId: 'driver_456',
startLocation: {
latitude: 40.7128,
longitude: -74.0060,
},
metadata: {
purpose: 'delivery',
platform: 'react-native',
},
};
await startTrip(request);
Alert.alert('Success', 'Trip started successfully');
} catch (err) {
Alert.alert('Error', 'Failed to start trip');
}
};
const handleStopTrip = async () => {
try {
await stopTrip();
Alert.alert('Success', 'Trip completed');
} catch (err) {
Alert.alert('Error', 'Failed to stop trip');
}
};
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 24, marginBottom: 20 }}>
Bookovia Trip Manager
</Text>
{currentTrip ? (
<TripStatusCard trip={currentTrip} />
) : (
<Text>No active trip</Text>
)}
<View style={{ flexDirection: 'row', marginTop: 20 }}>
<TouchableOpacity
style={[
styles.button,
{ backgroundColor: currentTrip ? '#ccc' : '#007AFF' }
]}
onPress={handleStartTrip}
disabled={!!currentTrip || isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? 'Starting...' : 'Start Trip'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.button,
{ backgroundColor: currentTrip ? '#FF3B30' : '#ccc' }
]}
onPress={handleStopTrip}
disabled={!currentTrip || isLoading}
>
<Text style={styles.buttonText}>Stop Trip</Text>
</TouchableOpacity>
</View>
{error && (
<Text style={{ color: 'red', marginTop: 10 }}>
Error: {error.message}
</Text>
)}
</View>
);
};
Location Tracking Hook
import { useEffect, useState } from 'react';
import { useLocationTracking, useBookovia } from '@bookovia/react-native-sdk';
import type { LocationPoint } from '@bookovia/react-native-sdk/types';
export const LocationTrackingScreen: React.FC = () => {
const { client } = useBookovia();
const {
currentLocation,
isTracking,
startTracking,
stopTracking,
requestPermission,
} = useLocationTracking();
const [tripId, setTripId] = useState<string | null>(null);
useEffect(() => {
requestPermission();
}, []);
const handleStartTracking = async () => {
if (!tripId) {
alert('Please start a trip first');
return;
}
await startTracking({
tripId,
interval: 30000, // 30 seconds
enableBackground: true,
});
};
const handleStopTracking = () => {
stopTracking();
};
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 20, marginBottom: 20 }}>
Location Tracking
</Text>
{currentLocation && (
<LocationCard location={currentLocation} />
)}
<View style={{ marginTop: 20 }}>
<TouchableOpacity
style={[styles.button, { backgroundColor: isTracking ? '#ccc' : '#34C759' }]}
onPress={handleStartTracking}
disabled={isTracking}
>
<Text style={styles.buttonText}>Start Tracking</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { backgroundColor: !isTracking ? '#ccc' : '#FF9500' }]}
onPress={handleStopTracking}
disabled={!isTracking}
>
<Text style={styles.buttonText}>Stop Tracking</Text>
</TouchableOpacity>
</View>
<Text style={{ marginTop: 20, textAlign: 'center' }}>
Status: {isTracking ? 'Tracking Active' : 'Not Tracking'}
</Text>
</View>
);
};
API Reference
Trip Query Methods
// User Trip Queries
await sdk.getUserActiveTrips('user_123');
await sdk.getUserTripHistory('user_123', 'completed');
// Organization Trip Queries
await sdk.getOrgActiveTrips('org_123');
await sdk.getOrgTripHistory('org_123', 'completed');
// Vehicle Trip Queries
await sdk.getVehicleActiveTrips('vehicle_123');
await sdk.getVehicleTripHistory('vehicle_123', 'completed');
Hooks
useTripManager
import { useTripManager } from '@bookovia/react-native-sdk';
const {
currentTrip,
isLoading,
error,
startTrip,
stopTrip,
pauseTrip,
resumeTrip,
getTripSummary,
} = useTripManager();
// Start a trip
await startTrip({
vehicleId: 'vehicle_123',
driverId: 'driver_456',
});
// Stop current trip
await stopTrip();
// Get trip summary
const summary = await getTripSummary('trip_id');
useLocationTracking
import { useLocationTracking } from '@bookovia/react-native-sdk';
const {
currentLocation,
isTracking,
hasPermission,
startTracking,
stopTracking,
requestPermission,
uploadLocation,
} = useLocationTracking();
// Request location permission
await requestPermission();
// Start location tracking
await startTracking({
tripId: 'trip_123',
interval: 30000,
enableBackground: true,
});
// Stop tracking
stopTracking();
useSafetyAnalytics
import { useSafetyAnalytics } from '@bookovia/react-native-sdk';
const {
safetyScore,
harshEvents,
behaviorAnalysis,
getSafetyScore,
getHarshEvents,
analyzeBehavior,
} = useSafetyAnalytics();
// Get safety score for driver
const score = await getSafetyScore({
driverId: 'driver_456',
dateRange: {
start: new Date('2024-04-01'),
end: new Date('2024-04-13'),
},
});
// Get harsh events
const events = await getHarshEvents({
tripId: 'trip_123',
eventType: 'harsh_braking',
});
useOfflineSync
import { useOfflineSync } from '@bookovia/react-native-sdk';
const {
isOnline,
pendingUploads,
syncProgress,
syncAll,
clearPendingData,
} = useOfflineSync();
// Manually trigger sync
await syncAll();
// Clear local pending data
await clearPendingData();
Client Methods
import { useBookovia } from '@bookovia/react-native-sdk';
const { client } = useBookovia();
// Trip operations
const trip = await client.trips.start(request);
const stoppedTrip = await client.trips.stop('trip_id');
const tripDetails = await client.trips.get('trip_id');
const trips = await client.trips.list(filters);
// Location operations
await client.locations.upload(locationRequest);
await client.locations.batchUpload(batchRequest);
const route = await client.locations.getRoute('trip_id');
// Safety analytics
const score = await client.safety.getScore(scoreRequest);
const analysis = await client.safety.analyzeBehavior(analysisRequest);
const events = await client.safety.getHarshEvents(filters);
// Fleet management
const overview = await client.fleet.getOverview();
const vehicles = await client.fleet.getVehicles();
const utilization = await client.fleet.getUtilization('vehicle_id');
Data Types
Core Types
export interface Trip {
tripId: string;
organizationId: string;
vehicleId: string;
driverId?: string;
status: TripStatus;
startTime: string;
endTime?: string;
startLocation?: Location;
endLocation?: Location;
analytics: TripAnalytics;
metadata?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface Location {
latitude: number;
longitude: number;
address?: string;
}
export interface LocationPoint {
latitude: number;
longitude: number;
timestamp: string;
speed: number;
heading: number;
accuracy?: number;
altitude?: number;
}
export enum TripStatus {
ACTIVE = 'active',
COMPLETED = 'completed',
PAUSED = 'paused',
CANCELLED = 'cancelled',
}
export interface TripAnalytics {
distanceKm: number;
durationMinutes: number;
maxSpeedKmh: number;
avgSpeedKmh: number;
idleTimeMinutes: number;
locationsCount: number;
eventsCount: number;
safetyScore: number;
ecoScore: number;
}
Request Types
export interface StartTripRequest {
vehicleId: string;
driverId?: string;
startLocation?: Location;
metadata?: Record<string, any>;
odometerReading?: number;
fuelLevelPercent?: number;
}
export interface StopTripRequest {
endLocation?: Location;
odometerReading?: number;
fuelLevelPercent?: number;
metadata?: Record<string, any>;
}
export interface LocationUploadRequest {
tripId: string;
latitude: number;
longitude: number;
timestamp: string;
speed: number;
heading: number;
accuracy?: number;
altitude?: number;
}
export interface BatchUploadRequest {
tripId: string;
locations: LocationPoint[];
}
export interface TripFilters {
vehicleId?: string;
driverId?: string;
status?: TripStatus;
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
}
Components
TripStatusCard Component
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import type { Trip } from '@bookovia/react-native-sdk/types';
interface TripStatusCardProps {
trip: Trip;
}
export const TripStatusCard: React.FC<TripStatusCardProps> = ({ trip }) => {
const getStatusColor = () => {
switch (trip.status) {
case 'active':
return '#34C759';
case 'completed':
return '#007AFF';
case 'paused':
return '#FF9500';
case 'cancelled':
return '#FF3B30';
default:
return '#8E8E93';
}
};
return (
<View style={[styles.card, { borderLeftColor: getStatusColor() }]}>
<View style={styles.header}>
<Text style={[styles.status, { color: getStatusColor() }]}>
{trip.status.toUpperCase()}
</Text>
<Text style={styles.tripId}>ID: {trip.tripId}</Text>
</View>
<View style={styles.metrics}>
<MetricItem
label="Distance"
value={`${trip.analytics.distanceKm.toFixed(1)} km`}
/>
<MetricItem
label="Duration"
value={`${trip.analytics.durationMinutes} min`}
/>
<MetricItem
label="Safety Score"
value={`${trip.analytics.safetyScore}/100`}
/>
</View>
<Text style={styles.vehicleInfo}>
Vehicle: {trip.vehicleId}
{trip.driverId && ` | Driver: ${trip.driverId}`}
</Text>
</View>
);
};
const MetricItem: React.FC<{ label: string; value: string }> = ({ label, value }) => (
<View style={styles.metric}>
<Text style={styles.metricValue}>{value}</Text>
<Text style={styles.metricLabel}>{label}</Text>
</View>
);
const styles = StyleSheet.create({
card: {
backgroundColor: 'white',
borderRadius: 12,
padding: 16,
borderLeftWidth: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
status: {
fontSize: 16,
fontWeight: 'bold',
},
tripId: {
fontSize: 12,
color: '#8E8E93',
},
metrics: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 12,
},
metric: {
alignItems: 'center',
},
metricValue: {
fontSize: 18,
fontWeight: '600',
color: '#1C1C1E',
},
metricLabel: {
fontSize: 12,
color: '#8E8E93',
marginTop: 2,
},
vehicleInfo: {
fontSize: 14,
color: '#48484A',
textAlign: 'center',
},
});
LocationCard Component
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import type { LocationPoint } from '@bookovia/react-native-sdk/types';
interface LocationCardProps {
location: LocationPoint;
}
export const LocationCard: React.FC<LocationCardProps> = ({ location }) => {
const formatCoordinate = (coord: number, isLatitude: boolean) => {
const direction = isLatitude
? (coord >= 0 ? 'N' : 'S')
: (coord >= 0 ? 'E' : 'W');
return `${Math.abs(coord).toFixed(6)}° ${direction}`;
};
return (
<View style={styles.card}>
<Text style={styles.title}>Current Location</Text>
<View style={styles.coordinates}>
<Text style={styles.coordinate}>
{formatCoordinate(location.latitude, true)}
</Text>
<Text style={styles.coordinate}>
{formatCoordinate(location.longitude, false)}
</Text>
</View>
<View style={styles.details}>
<DetailItem label="Speed" value={`${location.speed.toFixed(1)} km/h`} />
<DetailItem label="Heading" value={`${location.heading.toFixed(0)}°`} />
{location.accuracy && (
<DetailItem label="Accuracy" value={`${location.accuracy.toFixed(0)}m`} />
)}
</View>
<Text style={styles.timestamp}>
Updated: {new Date(location.timestamp).toLocaleTimeString()}
</Text>
</View>
);
};
const DetailItem: React.FC<{ label: string; value: string }> = ({ label, value }) => (
<View style={styles.detailItem}>
<Text style={styles.detailLabel}>{label}</Text>
<Text style={styles.detailValue}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
card: {
backgroundColor: 'white',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
title: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
textAlign: 'center',
},
coordinates: {
alignItems: 'center',
marginBottom: 16,
},
coordinate: {
fontSize: 16,
fontFamily: 'monospace',
color: '#007AFF',
marginBottom: 4,
},
details: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 12,
},
detailItem: {
alignItems: 'center',
},
detailLabel: {
fontSize: 12,
color: '#8E8E93',
},
detailValue: {
fontSize: 14,
fontWeight: '500',
color: '#1C1C1E',
},
timestamp: {
fontSize: 12,
color: '#8E8E93',
textAlign: 'center',
},
});
Background Processing
Background Location Service
// BackgroundLocationService.ts
import { AppState, Platform } from 'react-native';
import BackgroundJob from 'react-native-background-job';
import { BookoviaClient } from '@bookovia/react-native-sdk';
export class BackgroundLocationService {
private static instance: BackgroundLocationService;
private client: BookoviaClient;
private locationInterval?: NodeJS.Timeout;
private isRunning = false;
private constructor(client: BookoviaClient) {
this.client = client;
this.setupAppStateListener();
}
static getInstance(client: BookoviaClient): BackgroundLocationService {
if (!BackgroundLocationService.instance) {
BackgroundLocationService.instance = new BackgroundLocationService(client);
}
return BackgroundLocationService.instance;
}
start(tripId: string) {
if (this.isRunning) return;
this.isRunning = true;
if (Platform.OS === 'ios') {
this.startIOSBackgroundTask(tripId);
} else {
this.startAndroidBackgroundService(tripId);
}
}
stop() {
if (!this.isRunning) return;
this.isRunning = false;
if (this.locationInterval) {
clearInterval(this.locationInterval);
this.locationInterval = undefined;
}
BackgroundJob.stop();
}
private startIOSBackgroundTask(tripId: string) {
BackgroundJob.start({
jobKey: 'bookovia_location_tracking',
period: 30000, // 30 seconds
});
this.startLocationTracking(tripId);
}
private startAndroidBackgroundService(tripId: string) {
// Android foreground service implementation
// This would typically use a native module
this.startLocationTracking(tripId);
}
private startLocationTracking(tripId: string) {
this.locationInterval = setInterval(async () => {
try {
const location = await this.getCurrentLocation();
if (location) {
await this.client.locations.upload({
tripId,
latitude: location.latitude,
longitude: location.longitude,
timestamp: new Date().toISOString(),
speed: location.speed || 0,
heading: location.heading || 0,
accuracy: location.accuracy,
});
}
} catch (error) {
console.error('Background location upload failed:', error);
}
}, 30000);
}
private async getCurrentLocation(): Promise<LocationPoint | null> {
// Implementation depends on your location library
// This is a placeholder
return null;
}
private setupAppStateListener() {
AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background' && this.isRunning) {
// App went to background, ensure background task is running
console.log('App backgrounded, maintaining location tracking');
} else if (nextAppState === 'active' && this.isRunning) {
// App became active, location tracking continues
console.log('App foregrounded, location tracking active');
}
});
}
}
Offline Data Management
import AsyncStorage from '@react-native-async-storage/async-storage';
import NetInfo from '@react-native-community/netinfo';
import type { LocationPoint, Trip } from '@bookovia/react-native-sdk/types';
export class OfflineDataManager {
private static instance: OfflineDataManager;
private client: BookoviaClient;
private constructor(client: BookoviaClient) {
this.client = client;
this.setupNetworkListener();
}
static getInstance(client: BookoviaClient): OfflineDataManager {
if (!OfflineDataManager.instance) {
OfflineDataManager.instance = new OfflineDataManager(client);
}
return OfflineDataManager.instance;
}
async storeLocationOffline(location: LocationPoint): Promise<void> {
try {
const existingData = await AsyncStorage.getItem('offline_locations');
const locations: LocationPoint[] = existingData ? JSON.parse(existingData) : [];
locations.push(location);
await AsyncStorage.setItem('offline_locations', JSON.stringify(locations));
} catch (error) {
console.error('Failed to store location offline:', error);
}
}
async storeTripOffline(trip: Partial<Trip>): Promise<void> {
try {
const existingData = await AsyncStorage.getItem('offline_trips');
const trips: Partial<Trip>[] = existingData ? JSON.parse(existingData) : [];
trips.push(trip);
await AsyncStorage.setItem('offline_trips', JSON.stringify(trips));
} catch (error) {
console.error('Failed to store trip offline:', error);
}
}
async syncOfflineData(): Promise<void> {
try {
await this.syncOfflineLocations();
await this.syncOfflineTrips();
} catch (error) {
console.error('Offline sync failed:', error);
}
}
private async syncOfflineLocations(): Promise<void> {
const locationsData = await AsyncStorage.getItem('offline_locations');
if (!locationsData) return;
const locations: LocationPoint[] = JSON.parse(locationsData);
if (locations.length === 0) return;
// Group by tripId for batch uploads
const locationsByTrip = locations.reduce((acc, location) => {
const tripId = (location as any).tripId;
if (!acc[tripId]) acc[tripId] = [];
acc[tripId].push(location);
return acc;
}, {} as Record<string, LocationPoint[]>);
for (const [tripId, tripLocations] of Object.entries(locationsByTrip)) {
try {
await this.client.locations.batchUpload({
tripId,
locations: tripLocations,
});
} catch (error) {
console.error(`Failed to sync locations for trip ${tripId}:`, error);
}
}
// Clear synced data
await AsyncStorage.removeItem('offline_locations');
}
private async syncOfflineTrips(): Promise<void> {
const tripsData = await AsyncStorage.getItem('offline_trips');
if (!tripsData) return;
const trips: Partial<Trip>[] = JSON.parse(tripsData);
const syncedTrips: string[] = [];
for (const trip of trips) {
try {
// Sync trip data with server
if (trip.tripId) {
await this.client.trips.get(trip.tripId);
syncedTrips.push(trip.tripId);
}
} catch (error) {
console.error(`Failed to sync trip ${trip.tripId}:`, error);
}
}
// Remove synced trips
const remainingTrips = trips.filter(trip =>
!syncedTrips.includes(trip.tripId!)
);
await AsyncStorage.setItem('offline_trips', JSON.stringify(remainingTrips));
}
private setupNetworkListener(): void {
NetInfo.addEventListener(state => {
if (state.isConnected && state.isInternetReachable) {
console.log('Network connected, starting sync...');
this.syncOfflineData();
}
});
}
async getPendingDataCount(): Promise<{ locations: number; trips: number }> {
try {
const [locationsData, tripsData] = await Promise.all([
AsyncStorage.getItem('offline_locations'),
AsyncStorage.getItem('offline_trips'),
]);
const locations = locationsData ? JSON.parse(locationsData).length : 0;
const trips = tripsData ? JSON.parse(tripsData).length : 0;
return { locations, trips };
} catch (error) {
console.error('Failed to get pending data count:', error);
return { locations: 0, trips: 0 };
}
}
}
Testing
Unit Tests
import { renderHook, act } from '@testing-library/react-hooks';
import { useTripManager } from '@bookovia/react-native-sdk';
import { BookoviaProvider } from '@bookovia/react-native-sdk';
// Mock the BookoviaClient
jest.mock('@bookovia/react-native-sdk', () => ({
...jest.requireActual('@bookovia/react-native-sdk'),
BookoviaClient: jest.fn().mockImplementation(() => ({
trips: {
start: jest.fn(),
stop: jest.fn(),
get: jest.fn(),
},
})),
}));
describe('useTripManager', () => {
const wrapper = ({ children }: any) => (
<BookoviaProvider config={{ apiKey: 'test_key' }}>
{children}
</BookoviaProvider>
);
test('should start trip successfully', async () => {
const mockTrip = {
tripId: 'trip_123',
status: 'active',
vehicleId: 'vehicle_123',
};
const mockClient = {
trips: {
start: jest.fn().mockResolvedValue(mockTrip),
},
};
const { result } = renderHook(() => useTripManager(), { wrapper });
await act(async () => {
await result.current.startTrip({
vehicleId: 'vehicle_123',
});
});
expect(result.current.currentTrip).toEqual(mockTrip);
expect(result.current.isLoading).toBe(false);
});
test('should handle start trip error', async () => {
const mockError = new Error('Network error');
const mockClient = {
trips: {
start: jest.fn().mockRejectedValue(mockError),
},
};
const { result } = renderHook(() => useTripManager(), { wrapper });
await act(async () => {
try {
await result.current.startTrip({
vehicleId: 'vehicle_123',
});
} catch (error) {
// Expected error
}
});
expect(result.current.error).toBeTruthy();
expect(result.current.currentTrip).toBe(null);
});
});
Integration Tests
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { TripScreen } from '../src/screens/TripScreen';
import { BookoviaProvider } from '@bookovia/react-native-sdk';
describe('TripScreen Integration', () => {
const renderWithProvider = (component: React.ReactElement) => {
return render(
<BookoviaProvider config={{ apiKey: 'test_key' }}>
{component}
</BookoviaProvider>
);
};
test('should complete full trip lifecycle', async () => {
const { getByText, queryByText } = renderWithProvider(<TripScreen />);
// Initially no trip
expect(queryByText('No active trip')).toBeTruthy();
// Start trip
fireEvent.press(getByText('Start Trip'));
// Wait for trip to start
await waitFor(() => {
expect(queryByText('Trip ACTIVE')).toBeTruthy();
});
// Stop trip
fireEvent.press(getByText('Stop Trip'));
// Wait for trip to stop
await waitFor(() => {
expect(queryByText('No active trip')).toBeTruthy();
});
});
});
E2E Tests (with Detox)
import { device, expect, element, by } from 'detox';
describe('Bookovia Trip Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
it('should start and stop a trip', async () => {
// Navigate to trip screen
await element(by.text('Trips')).tap();
// Start trip
await element(by.id('start-trip-button')).tap();
// Fill vehicle ID
await element(by.id('vehicle-id-input')).typeText('test_vehicle_001');
await element(by.text('Start')).tap();
// Verify trip started
await waitFor(element(by.text('Trip ACTIVE')))
.toBeVisible()
.withTimeout(5000);
// Stop trip
await element(by.id('stop-trip-button')).tap();
// Verify trip stopped
await waitFor(element(by.text('No active trip')))
.toBeVisible()
.withTimeout(5000);
});
it('should track location during trip', async () => {
// Start trip
await element(by.id('start-trip-button')).tap();
await element(by.id('vehicle-id-input')).typeText('test_vehicle_001');
await element(by.text('Start')).tap();
// Navigate to location screen
await element(by.text('Location')).tap();
// Start location tracking
await element(by.id('start-tracking-button')).tap();
// Verify tracking started
await waitFor(element(by.text('Tracking Active')))
.toBeVisible()
.withTimeout(3000);
// Wait for location update
await waitFor(element(by.id('current-location')))
.toBeVisible()
.withTimeout(10000);
// Stop tracking
await element(by.id('stop-tracking-button')).tap();
// Verify tracking stopped
await waitFor(element(by.text('Not Tracking')))
.toBeVisible()
.withTimeout(3000);
});
});
Performance Optimization
Optimized Location Batching
class LocationBuffer {
private buffer: LocationPoint[] = [];
private readonly maxSize: number;
private readonly flushInterval: number;
private flushTimer?: NodeJS.Timeout;
private client: BookoviaClient;
constructor(client: BookoviaClient, maxSize = 10, flushInterval = 30000) {
this.client = client;
this.maxSize = maxSize;
this.flushInterval = flushInterval;
this.startFlushTimer();
}
addLocation(tripId: string, location: LocationPoint): void {
this.buffer.push({ ...location, tripId } as any);
if (this.buffer.length >= this.maxSize) {
this.flush(tripId);
}
}
private async flush(tripId: string): Promise<void> {
if (this.buffer.length === 0) return;
const locationsToFlush = [...this.buffer];
this.buffer = [];
try {
await this.client.locations.batchUpload({
tripId,
locations: locationsToFlush,
});
} catch (error) {
console.error('Failed to flush location buffer:', error);
// Re-add locations to buffer for retry
this.buffer.unshift(...locationsToFlush);
}
}
private startFlushTimer(): void {
this.flushTimer = setInterval(() => {
if (this.buffer.length > 0) {
// Get tripId from first location
const tripId = (this.buffer[0] as any).tripId;
this.flush(tripId);
}
}, this.flushInterval);
}
destroy(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer);
}
}
}
Platform-Specific Features
Android Native Module
// android/app/src/main/java/LocationModule.java
package com.bookovia.reactnative;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
public class LocationModule extends ReactContextBaseJavaModule {
private static final String NAME = "BookoviaLocation";
public LocationModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
@ReactMethod
public void startForegroundService(String tripId, Promise promise) {
try {
// Start Android foreground service for location tracking
Intent serviceIntent = new Intent(getCurrentActivity(), LocationTrackingService.class);
serviceIntent.putExtra("tripId", tripId);
getCurrentActivity().startForegroundService(serviceIntent);
promise.resolve(true);
} catch (Exception e) {
promise.reject("START_SERVICE_ERROR", e);
}
}
@ReactMethod
public void stopForegroundService(Promise promise) {
try {
Intent serviceIntent = new Intent(getCurrentActivity(), LocationTrackingService.class);
getCurrentActivity().stopService(serviceIntent);
promise.resolve(true);
} catch (Exception e) {
promise.reject("STOP_SERVICE_ERROR", e);
}
}
}
iOS Native Module
// ios/LocationModule.m
#import <React/RCTBridgeModule.h>
#import <CoreLocation/CoreLocation.h>
@interface LocationModule : NSObject <RCTBridgeModule, CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *locationManager;
@end
@implementation LocationModule
RCT_EXPORT_MODULE(BookoviaLocation);
RCT_EXPORT_METHOD(startBackgroundLocationTracking:(NSString *)tripId
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.allowsBackgroundLocationUpdates = YES;
self.locationManager.pausesLocationUpdatesAutomatically = NO;
[self.locationManager startUpdatingLocation];
resolve(@YES);
}
RCT_EXPORT_METHOD(stopBackgroundLocationTracking:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.locationManager stopUpdatingLocation];
self.locationManager.allowsBackgroundLocationUpdates = NO;
resolve(@YES);
}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
// Handle location updates and send to React Native
CLLocation *location = locations.lastObject;
NSDictionary *locationData = @{
@"latitude": @(location.coordinate.latitude),
@"longitude": @(location.coordinate.longitude),
@"timestamp": @([location.timestamp timeIntervalSince1970] * 1000),
@"speed": @(location.speed),
@"heading": @(location.course),
@"accuracy": @(location.horizontalAccuracy)
};
// Send event to React Native
[self sendEventWithName:@"LocationUpdate" body:locationData];
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"LocationUpdate"];
}
@end
Example Apps
Complete Trip Management App
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { BookoviaProvider } from '@bookovia/react-native-sdk';
import TripScreen from './src/screens/TripScreen';
import LocationScreen from './src/screens/LocationScreen';
import AnalyticsScreen from './src/screens/AnalyticsScreen';
const Tab = createBottomTabNavigator();
const bookoviaConfig = {
apiKey: 'bkv_live_your_api_key_here',
baseUrl: 'https://api.bookovia.com/v1',
enableOfflineMode: true,
enableDebugLogging: __DEV__,
};
export default function App() {
return (
<BookoviaProvider config={bookoviaConfig}>
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="Trips" component={TripScreen} />
<Tab.Screen name="Location" component={LocationScreen} />
<Tab.Screen name="Analytics" component={AnalyticsScreen} />
</Tab.Navigator>
</NavigationContainer>
</BookoviaProvider>
);
}
Support
- GitHub: github.com/bookovia/react-native-sdk
- npm: npmjs.com/package/@bookovia/react-native-sdk
- Issues: GitHub Issues
- Email: reactnative-support@bookovia.com
- Discord: React Native Community
Ready to build cross-platform telematics apps? Check out our quickstart guide and API reference for more examples.