Skip to main content

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
Requirements:
  • React Native 0.70+
  • iOS 13.0+
  • Android API level 21+

Setup

Android Configuration

Add permissions to android/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 to ios/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


Ready to build cross-platform telematics apps? Check out our quickstart guide and API reference for more examples.